Files
tgex-frontend/app/dashboard/[workspaceId]/page.tsx
2025-12-29 10:12:13 +03:00

232 lines
8.6 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";
// ============================================================================
// Dashboard Home Page
// ============================================================================
import { useEffect, useState } from "react";
import { Loader2, ShoppingCart, Users, TrendingUp, BarChart3 } 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 type { SpendingAnalytics } from "@/lib/types/api";
// ============================================================================
// Helpers
// ============================================================================
const formatNumber = (value: number | null | undefined, showSign = false): string => {
if (value === null || value === undefined) return "—";
const formatted = new Intl.NumberFormat("ru-RU").format(value);
if (showSign && value > 0) return `+${formatted}`;
return formatted;
};
const formatCurrency = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "—";
return new Intl.NumberFormat("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
// ============================================================================
// Component
// ============================================================================
export default function DashboardPage() {
const { currentWorkspace, currentProject, isLoading: workspaceLoading, projects } = useWorkspace();
const [spending, setSpending] = useState<SpendingAnalytics | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!currentWorkspace) {
setLoading(false);
return;
}
const loadData = async () => {
try {
setLoading(true);
const data = await demoAwareAnalyticsApi.spending(currentWorkspace.id, {
project_id: currentProject?.id,
});
setSpending(data);
} catch (err) {
console.error("Failed to load spending:", err);
} finally {
setLoading(false);
}
};
loadData();
}, [currentWorkspace?.id, currentProject?.id]);
if (workspaceLoading) {
return (
<>
<DashboardHeader breadcrumbs={[{ label: "Главная" }]} />
<div className="flex flex-1 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</>
);
}
if (!currentWorkspace) {
return (
<>
<DashboardHeader breadcrumbs={[{ label: "Главная" }]} />
<div className="flex flex-1 items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-semibold mb-2">Нет доступных воркспейсов</h2>
<p className="text-muted-foreground">
Создайте новый воркспейс для начала работы
</p>
</div>
</div>
</>
);
}
// Calculate totals and averages
const totalCost = spending?.total_cost ?? 0;
const totalSubscriptions = spending?.total_subscriptions ?? 0;
const totalViews = spending?.total_views ?? 0;
const avgCps = totalSubscriptions > 0 ? totalCost / totalSubscriptions : null;
const avgCpm = totalViews > 0 ? (totalCost / totalViews) * 1000 : null;
return (
<>
<DashboardHeader breadcrumbs={[{ label: "Главная" }]} />
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
{/* Stats 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>
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loading ? (
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
) : (
<div className="text-2xl font-bold">{formatCurrency(totalCost)}</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>
{loading ? (
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
) : (
<div className="text-2xl font-bold">{formatNumber(totalSubscriptions)}</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Средний CPS</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loading ? (
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
) : (
<div className="text-2xl font-bold">{formatCurrency(avgCps)}</div>
)}
</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>
{loading ? (
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
) : (
<div className="text-2xl font-bold">{formatCurrency(avgCpm)}</div>
)}
</CardContent>
</Card>
</div>
{/* Quick Actions */}
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Быстрый старт</CardTitle>
<CardDescription>Начните с основных действий</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<a
href={`/dashboard/${currentWorkspace.id}/placements/new`}
className="block text-sm text-primary hover:underline"
>
Создать размещение
</a>
<a
href={`/dashboard/${currentWorkspace.id}/creatives/new`}
className="block text-sm text-primary hover:underline"
>
Создать креатив
</a>
<a
href={`/dashboard/${currentWorkspace.id}/analytics`}
className="block text-sm text-primary hover:underline"
>
Посмотреть аналитику
</a>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Проекты</CardTitle>
<CardDescription>
{projects.length > 0
? `${projects.length} активных проектов`
: "Нет активных проектов"}
</CardDescription>
</CardHeader>
<CardContent>
{projects.length > 0 ? (
<div className="space-y-2">
{projects.slice(0, 5).map((project) => (
<div
key={project.id}
className="flex items-center justify-between text-sm"
>
<span className="font-medium">{project.title}</span>
{project.username && (
<span className="text-muted-foreground">@{project.username}</span>
)}
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">
Добавьте бота в канал через Telegram для создания проекта
</p>
)}
</CardContent>
</Card>
</div>
</div>
</>
);
}