Files
tgex-frontend/app/dashboard/[workspaceId]/analytics/page.tsx
2026-01-07 16:08:26 +03:00

283 lines
10 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 { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import {
Loader2,
Users,
Eye,
DollarSign,
ArrowRight,
BarChart3,
Info,
} from "lucide-react";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import { useWorkspace } from "@/components/providers/workspace-provider";
import { demoAwareAnalyticsApi } from "@/lib/demo";
import { ProjectSelector } from "@/components/project-selector";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import type { SpendingAnalytics } from "@/lib/types/api";
// ============================================================================
// Helpers
// ============================================================================
const formatCurrency = (value: number | null) => {
if (value === null) return "—";
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const formatNumber = (value: number | null) => {
if (value === null) return "—";
return new Intl.NumberFormat("ru-RU").format(value);
};
// ============================================================================
// Component
// ============================================================================
export default function AnalyticsPage() {
const params = useParams();
const workspaceId = params?.workspaceId as string;
const {
selectedProjects,
getProjectFilterId,
allProjectsSelected,
projects
} = useWorkspace();
const [spending, setSpending] = useState<SpendingAnalytics | null>(null);
const [loading, setLoading] = useState(true);
const projectFilterId = getProjectFilterId();
useEffect(() => {
loadAnalytics();
}, [workspaceId, projectFilterId]);
const loadAnalytics = async () => {
try {
setLoading(true);
const data = await demoAwareAnalyticsApi.spending(workspaceId, {
project_id: projectFilterId,
grouping: "month",
});
setSpending(data);
} catch (err) {
console.error("Failed to load analytics:", err);
} finally {
setLoading(false);
}
};
// Calculate averages
const avgCps =
spending && spending.total_subscriptions > 0
? spending.total_cost / spending.total_subscriptions
: null;
const avgCpm =
spending && spending.total_views > 0
? (spending.total_cost / spending.total_views) * 1000
: null;
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: `/dashboard/${workspaceId}` },
{ label: "Аналитика" },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Аналитика</h1>
<p className="text-muted-foreground">
Обзор эффективности рекламных размещений
</p>
</div>
<ProjectSelector />
</div>
{selectedProjects.length > 1 && !allProjectsSelected && (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
Выбрано {selectedProjects.length} проекта: {selectedProjects.map(p => p.title).join(", ")}.
Показана суммарная аналитика по всем выбранным проектам.
</AlertDescription>
</Alert>
)}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<>
{/* Summary Cards */}
<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 ?? 0)}
</div>
</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 ?? 0)}
</div>
<p className="text-xs text-muted-foreground">
Средний CPS: {avgCps ? formatCurrency(avgCps) : "—"}
</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>
<Eye className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(spending?.total_views ?? 0)}
</div>
<p className="text-xs text-muted-foreground">
Средний CPM: {avgCpm ? formatCurrency(avgCpm) : "—"}
</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">
{spending?.chart_data?.length ?? 0}
</div>
<p className="text-xs text-muted-foreground">месяцев</p>
</CardContent>
</Card>
</div>
{/* Quick Links */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card className="hover:bg-muted/50 transition-colors">
<Link href={`/dashboard/${workspaceId}/analytics/spending`}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
Динамика расходов
<ArrowRight className="h-4 w-4" />
</CardTitle>
<CardDescription>
График расходов по времени с группировкой
</CardDescription>
</CardHeader>
</Link>
</Card>
<Card className="hover:bg-muted/50 transition-colors">
<Link href={`/dashboard/${workspaceId}/analytics/channels`}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
Аналитика по каналам
<ArrowRight className="h-4 w-4" />
</CardTitle>
<CardDescription>
Эффективность каналов размещения
</CardDescription>
</CardHeader>
</Link>
</Card>
<Card className="hover:bg-muted/50 transition-colors">
<Link href={`/dashboard/${workspaceId}/analytics/creatives`}>
<CardHeader>
<CardTitle className="flex items-center justify-between">
Аналитика по креативам
<ArrowRight className="h-4 w-4" />
</CardTitle>
<CardDescription>
Сравнение эффективности креативов
</CardDescription>
</CardHeader>
</Link>
</Card>
</div>
{/* Recent Activity */}
{spending && spending.chart_data && spending.chart_data.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Последние периоды</CardTitle>
<CardDescription>Динамика по периодам</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{spending.chart_data.slice(0, 6).map((item, index) => (
<div
key={index}
className="flex items-center justify-between border-b pb-4 last:border-0 last:pb-0"
>
<div>
<div className="font-medium">{item.period}</div>
<div className="text-sm text-muted-foreground">
{formatNumber(item.subscriptions)} подписчиков {" "}
{formatNumber(item.views)} просмотров
</div>
</div>
<div className="text-right">
<div className="font-medium">
{formatCurrency(item.cost)}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</>
)}
</div>
</>
);
}