292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
"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 {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
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;
|
||
|
||
// Selection info text
|
||
const selectionText = allProjectsSelected
|
||
? "Все проекты"
|
||
: selectedProjects.length === 1
|
||
? selectedProjects[0].title
|
||
: `${selectedProjects.length} проекта`;
|
||
|
||
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>
|
||
<Badge variant="outline" className="text-sm">
|
||
{selectionText}
|
||
</Badge>
|
||
</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>
|
||
</>
|
||
);
|
||
}
|