"use client"; // ============================================================================ // Projects Page (Target Channels) - Enhanced with Statistics // ============================================================================ import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import Link from "next/link"; import { Loader2, Tv, ExternalLink, Grid, List, TrendingUp, Users, Eye, ShoppingCart, Info, Bot, Settings, Folder, Archive, Trash2, } from "lucide-react"; import { DashboardHeader } from "@/components/layout/dashboard-header"; import { useWorkspace } from "@/components/providers/workspace-provider"; import { demoAwareAnalyticsApi, demoAwareProjectsApi } from "@/lib/demo"; import { projectsApi } from "@/lib/api"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { BOT_USERNAME } from "@/lib/utils/constants"; import type { Project, SpendingAnalytics } from "@/lib/types/api"; // ============================================================================ // Types // ============================================================================ interface ProjectWithStats extends Project { stats?: { total_cost: number; total_subscriptions: number; total_views: number; avg_cpf: number | null; }; } // ============================================================================ // 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); }; // ============================================================================ // Component // ============================================================================ export default function ProjectsPage() { const params = useParams(); const workspaceId = params?.workspaceId as string; const { projects, isLoadingProjects, currentWorkspace, refreshProjects } = useWorkspace(); const [projectsWithStats, setProjectsWithStats] = useState([]); const [loadingStats, setLoadingStats] = useState(false); const [viewMode, setViewMode] = useState<"grid" | "table">("grid"); const [settingsProject, setSettingsProject] = useState(null); const [inviteLinkType, setInviteLinkType] = useState<"public" | "approval">("public"); const [isUpdatingSettings, setIsUpdatingSettings] = useState(false); const [isArchiving, setIsArchiving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deleteConfirmationText, setDeleteConfirmationText] = useState(""); const { isDemoMode } = useWorkspace(); const handleOpenSettings = (project: Project) => { setSettingsProject(project); setInviteLinkType(project.purchase_invite_type_default || "public"); }; const handleSaveSettings = async () => { if (!settingsProject) return; try { setIsUpdatingSettings(true); const api = isDemoMode ? demoAwareProjectsApi : projectsApi; await api.updateInviteLinkType(workspaceId, settingsProject.id, { purchase_invite_type_default: inviteLinkType, }); await refreshProjects(); setSettingsProject(null); } catch (err) { console.error("Failed to update settings:", err); } finally { setIsUpdatingSettings(false); } }; const handleArchive = async () => { if (!settingsProject) return; try { setIsArchiving(true); const api = isDemoMode ? demoAwareProjectsApi : projectsApi; await api.archive(workspaceId, settingsProject.id); await refreshProjects(); setSettingsProject(null); } catch (err) { console.error("Failed to archive project:", err); } finally { setIsArchiving(false); } }; const handleDelete = async () => { if (!settingsProject) return; try { setIsDeleting(true); const api = isDemoMode ? demoAwareProjectsApi : projectsApi; await api.delete(workspaceId, settingsProject.id); await refreshProjects(); setSettingsProject(null); setShowDeleteDialog(false); setDeleteConfirmationText(""); } catch (err) { console.error("Failed to delete project:", err); } finally { setIsDeleting(false); } }; const handleOpenDeleteDialog = () => { setShowDeleteDialog(true); }; const handleCloseDeleteDialog = () => { setShowDeleteDialog(false); setDeleteConfirmationText(""); }; const getChannelAvatar = (project: Project) => { // Telegram channel avatar URL format if (project.username) { return `https://t.me/i/userpic/320/${project.username}.jpg`; } return null; }; // Load statistics for each project useEffect(() => { if (projects.length === 0) { setProjectsWithStats([]); return; } const loadStats = async () => { setLoadingStats(true); try { const statsPromises = projects.map(async (project) => { try { const spending = await demoAwareAnalyticsApi.spending(workspaceId, { project_id: project.id, }); return { ...project, stats: { total_cost: spending.total_cost, total_subscriptions: spending.total_subscriptions, total_views: spending.total_views, avg_cpf: spending.total_subscriptions > 0 ? spending.total_cost / spending.total_subscriptions : null, }, }; } catch { return { ...project, stats: undefined }; } }); const results = await Promise.all(statsPromises); setProjectsWithStats(results); } catch (err) { console.error("Failed to load project stats:", err); setProjectsWithStats(projects); } finally { setLoadingStats(false); } }; loadStats(); }, [projects, workspaceId]); const isLoading = isLoadingProjects || loadingStats; return (

Проекты

Telegram каналы, которые вы продвигаете

Чтобы добавить новый проект, добавьте бота{" "} @{BOT_USERNAME} {" "} администратором в ваш Telegram канал. {isLoading ? (
) : projectsWithStats.length === 0 ? (

Нет проектов

Добавьте бота администратором в ваш Telegram канал, и он автоматически появится здесь как проект.

) : viewMode === "table" ? ( // Table View Канал Статус
Подписчиков Количество подписчиков в канале
Привлечено подписчиков Привлеченные подписчики по пригласительным ссылкам
Суммарный охват Суммарные просмотры рекламных постов
CPF Cost Per Follower — средняя стоимость подписчика
{projectsWithStats.map((project) => (
{project.title}
{project.username && ( @{project.username} )}
{project.status === "active" ? "Активен" : project.status === "inactive" ? "Неактивен" : "Архив"} {formatNumber(project.subscribers_count)} {formatNumber(project.stats?.total_subscriptions)} {formatNumber(project.stats?.total_views)} {formatCurrency(project.stats?.avg_cpf)}
))}
) : ( // Grid View
{projectsWithStats.map((project) => (
{project.title} {project.username && ( @{project.username} )}
{project.status === "active" ? "Активен" : project.status === "inactive" ? "Неактивен" : "Архив"}
{/* Stats Grid */}
{project.subscribers_count !== undefined && (
{formatNumber(project.subscribers_count)}
Подписчиков
)} {project.stats && ( <>
{formatNumber(project.stats.total_subscriptions)}
Привлечено подписчиков
{formatNumber(project.stats.total_views)}
Суммарный охват
{formatCurrency(project.stats.avg_cpf)}
CPF
)}
{/* Actions */}
))}
)} {/* Settings Dialog */} !open && setSettingsProject(null)}> Настройки проекта {settingsProject?.title}

Тип пригласительной ссылки, который будет использоваться по умолчанию при создании размещений для этого проекта

{/* Danger Zone */}

Опасная зона

{settingsProject?.status !== "archived" && ( )}
{/* Delete Confirmation Dialog */} Удалить проект "{settingsProject?.title}"? Это действие необратимо. Все данные проекта будут удалены, включая креативы, размещения и аналитику.

Для подтверждения введите Удалить {settingsProject?.title} ниже:
setDeleteConfirmationText(e.target.value)} className="mt-4" />
Отмена {isDeleting ? ( <> Удаление... ) : ( "Удалить" )}
); }