Files
tgex-frontend/app/(dashboard)/channels/[id]/page.tsx
2025-11-10 12:07:24 +03:00

361 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// ============================================================================
// Target Channel Detail Page
// ============================================================================
import React, { useEffect, useState } from "react";
import { use } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { channelsApi } from "@/lib/api";
import {
formatNumber,
formatMetric,
formatCurrency,
formatUsername,
formatDate,
} from "@/lib/utils/format";
import {
Target,
Users,
TrendingUp,
ArrowLeft,
Loader2,
AlertCircle,
ExternalLink as ExternalLinkIcon,
BarChart3,
} from "lucide-react";
import type { TargetChannelDetail } from "@/lib/types/api";
interface PageProps {
params: Promise<{ id: string }>;
}
export default function ChannelDetailPage({ params }: PageProps) {
const resolvedParams = use(params);
const router = useRouter();
const [channel, setChannel] = useState<TargetChannelDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadChannel = async () => {
try {
setLoading(true);
const data = await channelsApi.get(resolvedParams.id);
setChannel(data);
} catch (err: any) {
setError(err?.error?.message || "Ошибка загрузки канала");
} finally {
setLoading(false);
}
};
loadChannel();
}, [resolvedParams.id]);
const handleToggleActive = async () => {
if (!channel) return;
try {
const updated = await channelsApi.update(channel.id, {
is_active: !channel.is_active,
});
setChannel({ ...channel, is_active: updated.is_active });
} catch (err: any) {
alert(err?.error?.message || "Ошибка обновления канала");
}
};
const handleDelete = async () => {
if (!channel) return;
if (!confirm("Вы уверены, что хотите отключить этот канал?")) return;
try {
await channelsApi.delete(channel.id);
router.push("/channels");
} catch (err: any) {
alert(err?.error?.message || "Ошибка удаления канала");
}
};
if (loading) {
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Целевые каналы", href: "/channels" },
{ label: "Загрузка..." },
]}
/>
<div className="flex flex-1 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</>
);
}
if (error || !channel) {
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Целевые каналы", href: "/channels" },
{ label: "Ошибка" },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error || "Канал не найден"}</AlertDescription>
</Alert>
<Button asChild variant="outline">
<Link href="/channels">
<ArrowLeft className="mr-2 h-4 w-4" />
Вернуться к каналам
</Link>
</Button>
</div>
</>
);
}
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Целевые каналы", href: "/channels" },
{ label: channel.title },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Target className="h-6 w-6" />
<h1 className="text-3xl font-bold tracking-tight">
{channel.title}
</h1>
<Badge variant={channel.is_active ? "default" : "secondary"}>
{channel.is_active ? "Активен" : "Отключен"}
</Badge>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
{channel.username ? (
<a
href={`https://t.me/${channel.username.replace("@", "")}`}
target="_blank"
rel="noopener noreferrer"
className="hover:underline flex items-center gap-1"
>
{formatUsername(channel.username)}
<ExternalLinkIcon className="h-3 w-3" />
</a>
) : (
<span>Приватный канал</span>
)}
<span></span>
<span>Добавлен {formatDate(channel.created_at)}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleToggleActive}>
{channel.is_active ? "Отключить" : "Включить"}
</Button>
<Button variant="destructive" onClick={handleDelete}>
Удалить
</Button>
</div>
</div>
{channel.description && (
<Card>
<CardHeader>
<CardTitle>Описание</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{channel.description}</p>
</CardContent>
</Card>
)}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Всего закупов
</CardTitle>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(channel.total_purchases)}
</div>
<p className="text-xs text-muted-foreground mt-1">
За весь период
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Подписчиков привлечено
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(channel.total_subscriptions)}
</div>
<p className="text-xs text-muted-foreground mt-1">Всего</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Средний CPF</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatMetric(channel.avg_cpf)}
</div>
<p className="text-xs text-muted-foreground mt-1">
Стоимость подписчика
</p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="stats" className="w-full">
<TabsList>
<TabsTrigger value="stats">Статистика по периодам</TabsTrigger>
<TabsTrigger value="purchases">Последние закупы</TabsTrigger>
</TabsList>
<TabsContent value="stats" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Статистика по периодам</CardTitle>
<CardDescription>
Динамика привлечения подписчиков
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Период</TableHead>
<TableHead className="text-right">Подписчики</TableHead>
<TableHead className="text-right">Затраты</TableHead>
<TableHead className="text-right">CPF</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channel.stats_by_period.map((stat, index) => (
<TableRow key={index}>
<TableCell className="font-medium capitalize">
{stat.period}
</TableCell>
<TableCell className="text-right">
{formatNumber(stat.subscriptions)}
</TableCell>
<TableCell className="text-right">
{formatCurrency(stat.cost)}
</TableCell>
<TableCell className="text-right">
{formatMetric(stat.cpf)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="purchases" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Последние закупы</CardTitle>
<CardDescription>
5 последних рекламных размещений
</CardDescription>
</CardHeader>
<CardContent>
{channel.recent_purchases.length === 0 ? (
<p className="text-center text-muted-foreground py-8">
Закупов пока нет
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Внешний канал</TableHead>
<TableHead>Дата</TableHead>
<TableHead className="text-right">Подписчики</TableHead>
<TableHead className="text-right">CPF</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channel.recent_purchases.map((purchase) => (
<TableRow key={purchase.id}>
<TableCell className="font-medium">
{purchase.external_channel.title}
</TableCell>
<TableCell>
{formatDate(
purchase.actual_date || purchase.scheduled_date
)}
</TableCell>
<TableCell className="text-right">
{formatNumber(purchase.subscriptions_count)}
</TableCell>
<TableCell className="text-right">
{formatMetric(purchase.cpf)}
</TableCell>
<TableCell className="text-right">
<Button asChild variant="ghost" size="sm">
<Link href={`/purchases/${purchase.id}`}>
Открыть
</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</>
);
}