Files
tgex-frontend/app/(dashboard)/analytics/page.tsx

243 lines
7.5 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";
// ============================================================================
// Analytics Overview Page
// ============================================================================
import React, { useEffect, useState } from "react";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { analyticsApi } from "@/lib/api";
import {
formatNumber,
formatMetric,
formatCurrency,
formatPercent,
} from "@/lib/utils/format";
import {
BarChart3,
Loader2,
AlertCircle,
TrendingUp,
TrendingDown,
Users,
ShoppingCart,
DollarSign,
} from "lucide-react";
import type { SpendingAnalytics } from "@/lib/types/api";
import { useTargetChannel } from "@/components/providers/target-channel-provider";
import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
export default function AnalyticsPage() {
const { selectedChannel } = useTargetChannel();
const [spending, setSpending] = useState<SpendingAnalytics | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (selectedChannel) {
loadData();
} else {
setLoading(false);
}
}, [selectedChannel]);
const loadData = async () => {
if (!selectedChannel) return;
try {
setLoading(true);
const data = await analyticsApi.spending({
grouping: "week",
target_channel_id: selectedChannel.id,
});
setSpending(data);
} catch (err: any) {
setError(err?.error?.message || "Ошибка загрузки аналитики");
} finally {
setLoading(false);
}
};
if (loading) {
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Аналитика" },
]}
/>
<div className="flex flex-1 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</>
);
}
if (error || !spending) {
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: "/" },
{ 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>
</div>
</>
);
}
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Аналитика" },
{ label: "Обзор" },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Аналитика</h1>
<p className="text-muted-foreground mt-1">
Сводная статистика за последний месяц
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg: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>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatCurrency(spending.total_cost)}
</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(spending.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>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{spending.avg_cpf ? `${formatMetric(spending.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>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{spending.avg_cpm ? `${formatMetric(spending.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>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={spending.chart_data}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="period"
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
/>
<YAxis
className="text-xs"
tick={{ fill: "hsl(var(--muted-foreground))" }}
tickFormatter={(value) => `${formatNumber(value)}`}
/>
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--background))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
/>
<Line
type="monotone"
dataKey="cost"
stroke="hsl(var(--primary))"
strokeWidth={2}
name="Затраты"
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</>
);
}