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

321 lines
11 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";
// ============================================================================
// External 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 { Alert, AlertDescription } from "@/components/ui/alert";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { externalChannelsApi } from "@/lib/api";
import {
formatNumber,
formatMetric,
formatCurrency,
formatUsername,
formatDate,
formatCompactNumber,
} from "@/lib/utils/format";
import {
ExternalLink as ExternalLinkIcon,
Users,
TrendingUp,
ArrowLeft,
Loader2,
AlertCircle,
BarChart3,
Pencil,
Trash2,
} from "lucide-react";
import type { ExternalChannelDetail } from "@/lib/types/api";
interface PageProps {
params: Promise<{ id: string }>;
}
export default function ExternalChannelDetailPage({ params }: PageProps) {
const resolvedParams = use(params);
const router = useRouter();
const [channel, setChannel] = useState<ExternalChannelDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadChannel = async () => {
try {
setLoading(true);
const data = await externalChannelsApi.get(resolvedParams.id);
setChannel(data);
} catch (err: any) {
setError(err?.error?.message || "Ошибка загрузки канала");
} finally {
setLoading(false);
}
};
loadChannel();
}, [resolvedParams.id]);
const handleDelete = async () => {
if (!channel) return;
if (!confirm(`Удалить канал "${channel.title}"?`)) return;
try {
await externalChannelsApi.delete(channel.id);
router.push("/external-channels");
} catch (err: any) {
alert(err?.error?.message || "Ошибка удаления канала");
}
};
if (loading) {
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Внешние каналы", href: "/external-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: "/external-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="/external-channels">
<ArrowLeft className="mr-2 h-4 w-4" />
Вернуться к каналам
</Link>
</Button>
</div>
</>
);
}
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Внешние каналы", href: "/external-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">
<ExternalLinkIcon className="h-6 w-6" />
<h1 className="text-3xl font-bold tracking-tight">
{channel.title}
</h1>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
{channel.username ? (
<a
href={channel.link}
target="_blank"
rel="noopener noreferrer"
className="hover:underline flex items-center gap-1"
>
{formatUsername(channel.username)}
<ExternalLinkIcon className="h-3 w-3" />
</a>
) : (
<a
href={channel.link}
target="_blank"
rel="noopener noreferrer"
className="hover:underline flex items-center gap-1"
>
{channel.link}
<ExternalLinkIcon className="h-3 w-3" />
</a>
)}
<span></span>
<span>Добавлен {formatDate(channel.created_at)}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleDelete}>
<Trash2 className="mr-2 h-4 w-4" />
Удалить
</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-4">
<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">
{channel.subscribers_count
? formatCompactNumber(channel.subscribers_count)
: "—"}
</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>
<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">Средний 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>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Средний CPM</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatMetric(channel.avg_cpm)}
</div>
<p className="text-xs text-muted-foreground mt-1">
Стоимость 1000 просмотров
</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>История закупов</CardTitle>
<CardDescription>
Все рекламные размещения в этом канале
</CardDescription>
</CardHeader>
<CardContent>
{channel.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">Подписчики</TableHead>
<TableHead className="text-right">CPF</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channel.purchases.map((purchase) => (
<TableRow key={purchase.id}>
<TableCell className="font-medium">
{purchase.target_channel.title}
</TableCell>
<TableCell>
{formatDate(
purchase.actual_date || purchase.scheduled_date
)}
</TableCell>
<TableCell className="text-right">
{formatCurrency(purchase.cost)}
</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>
</div>
</>
);
}