fix: adding first channel fix

This commit is contained in:
ivannoskov
2025-12-01 08:03:45 +03:00
parent 2ea90d1b55
commit 8e6a1fa83f
2 changed files with 290 additions and 136 deletions

View File

@@ -4,7 +4,7 @@
// Dashboard Home Page // Dashboard Home Page
// ============================================================================ // ============================================================================
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import { DashboardHeader } from "@/components/layout/dashboard-header"; import { DashboardHeader } from "@/components/layout/dashboard-header";
import { import {
Card, Card,
@@ -13,7 +13,15 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { analyticsApi } from "@/lib/api"; import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { analyticsApi, channelsApi } from "@/lib/api";
import { import {
formatCurrency, formatCurrency,
formatNumber, formatNumber,
@@ -26,15 +34,87 @@ import {
Users, Users,
ShoppingCart, ShoppingCart,
Loader2, Loader2,
ExternalLink,
CheckCircle2,
} from "lucide-react"; } from "lucide-react";
import type { SpendingAnalytics } from "@/lib/types/api"; import type { SpendingAnalytics } from "@/lib/types/api";
import { useTargetChannel } from "@/components/providers/target-channel-provider"; import { useTargetChannel } from "@/components/providers/target-channel-provider";
import { BOT_USERNAME } from "@/lib/utils/constants";
export default function DashboardPage() { export default function DashboardPage() {
const { selectedChannel } = useTargetChannel(); const {
selectedChannel,
channels,
isLoading: channelsLoading,
} = useTargetChannel();
const [spending, setSpending] = useState<SpendingAnalytics | null>(null); const [spending, setSpending] = useState<SpendingAnalytics | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Для модального окна добавления канала
const [showAddChannelDialog, setShowAddChannelDialog] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [initialChannelCount, setInitialChannelCount] = useState(0);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Показываем модальное окно, если нет каналов
useEffect(() => {
if (!channelsLoading && channels.length === 0) {
setShowAddChannelDialog(true);
setInitialChannelCount(0);
}
}, [channelsLoading, channels]);
// Автоматический polling при открытом окне
useEffect(() => {
if (showAddChannelDialog && !isSuccess && initialChannelCount === 0) {
pollingIntervalRef.current = setInterval(() => {
checkForNewChannels();
}, 1000);
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}
}, [showAddChannelDialog, isSuccess, initialChannelCount]);
// Очистка при размонтировании
useEffect(() => {
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, []);
const checkForNewChannels = async () => {
try {
const response = await channelsApi.list();
const activeChannels = response.target_channels.filter(
(c) => c.is_active
);
if (activeChannels.length > initialChannelCount) {
setIsSuccess(true);
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
setTimeout(() => {
setShowAddChannelDialog(false);
setIsSuccess(false);
window.location.reload();
}, 2000);
}
} catch (err) {
console.error("Error polling channels:", err);
}
};
useEffect(() => { useEffect(() => {
if (!selectedChannel) { if (!selectedChannel) {
setLoading(false); setLoading(false);
@@ -59,10 +139,85 @@ export default function DashboardPage() {
loadData(); loadData();
}, [selectedChannel]); }, [selectedChannel]);
if (!selectedChannel) { return (
return ( <>
<> <DashboardHeader breadcrumbs={[{ label: "Главная" }]} />
<DashboardHeader breadcrumbs={[{ label: "Главная" }]} />
{/* Модальное окно добавления первого канала */}
<Dialog open={showAddChannelDialog} onOpenChange={() => {}}>
<DialogContent
className="sm:max-w-md"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>Добро пожаловать!</DialogTitle>
<DialogDescription>
Для начала работы добавьте ваш первый целевой канал
</DialogDescription>
</DialogHeader>
{isSuccess ? (
<div className="flex flex-col items-center justify-center py-8 space-y-4">
<div className="rounded-full bg-green-100 dark:bg-green-900 p-3">
<CheckCircle2 className="h-8 w-8 text-green-600 dark:text-green-400" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold">
Канал успешно добавлен!
</h3>
<p className="text-sm text-muted-foreground mt-1">
Страница обновится автоматически...
</p>
</div>
</div>
) : (
<div className="space-y-4">
<Alert>
<AlertDescription className="space-y-2">
<p className="font-medium">Шаги для добавления канала:</p>
<ol className="list-decimal list-inside space-y-1.5 text-sm">
<li>Откройте свой Telegram канал</li>
<li>Перейдите в настройки канала Администраторы</li>
<li>
Добавьте бота{" "}
<a
href={`https://t.me/${BOT_USERNAME}`}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-primary hover:underline inline-flex items-center gap-1"
>
@{BOT_USERNAME}
<ExternalLink className="h-3 w-3" />
</a>{" "}
как администратора
</li>
<li>
Предоставьте боту права:{" "}
<span className="font-medium">
Публикация сообщений, Удаление сообщений
</span>
</li>
</ol>
</AlertDescription>
</Alert>
<div className="flex flex-col items-center justify-center py-6 space-y-3">
<Loader2 className="h-10 w-10 animate-spin text-primary" />
<p className="text-sm text-muted-foreground text-center">
Ждем добавления бота...
<br />
<span className="text-xs">
Как только вы добавите бота, канал автоматически появится
</span>
</p>
</div>
</div>
)}
</DialogContent>
</Dialog>
{!selectedChannel ? (
<div className="flex flex-1 items-center justify-center"> <div className="flex flex-1 items-center justify-center">
<div className="text-center"> <div className="text-center">
<h2 className="text-2xl font-semibold mb-2"> <h2 className="text-2xl font-semibold mb-2">
@@ -73,139 +228,138 @@ export default function DashboardPage() {
</p> </p>
</div> </div>
</div> </div>
</> ) : (
); <div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
} <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">
{formatNumber(spending?.total_cost)}
</div>
<p className="text-xs text-muted-foreground">
{formatNumber(0 || 0, true)} за прошлый месяц
</p>
</>
)}
</CardContent>
</Card>
return ( <Card>
<> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<DashboardHeader breadcrumbs={[{ label: "Главная" }]} /> <CardTitle className="text-sm font-medium">
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4"> Подписчиков привлечено
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> </CardTitle>
<Card> <Users className="h-4 w-4 text-muted-foreground" />
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> </CardHeader>
<CardTitle className="text-sm font-medium"> <CardContent>
Всего закупов {loading ? (
</CardTitle> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<ShoppingCart className="h-4 w-4 text-muted-foreground" /> ) : (
</CardHeader> <>
<CardContent> <div className="text-2xl font-bold">
{loading ? ( {formatNumber(spending?.total_subscriptions)}
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> </div>
) : ( <p className="text-xs text-muted-foreground">
<> {formatNumber(0 || 0, true)} за прошлый месяц
<div className="text-2xl font-bold"> </p>
{formatNumber(spending?.total_cost)} </>
</div> )}
<p className="text-xs text-muted-foreground"> </CardContent>
{formatNumber(0 || 0, true)} за прошлый месяц </Card>
</p>
</>
)}
</CardContent>
</Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">
Подписчиков привлечено Средний CPF
</CardTitle> </CardTitle>
<Users className="h-4 w-4 text-muted-foreground" /> <TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{loading ? ( {loading ? (
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
) : ( ) : (
<> <>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{formatNumber(spending?.total_subscriptions)} {formatMetric(spending?.avg_cpf)}
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{formatNumber(0 || 0, true)} за прошлый месяц {formatPercent(0 || 0, true)} от прошлого месяца
</p> </p>
</> </>
)} )}
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Средний CPF</CardTitle> <CardTitle className="text-sm font-medium">
<TrendingUp className="h-4 w-4 text-muted-foreground" /> Средний CPM
</CardHeader> </CardTitle>
<CardContent> <BarChart3 className="h-4 w-4 text-muted-foreground" />
{loading ? ( </CardHeader>
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <CardContent>
) : ( {loading ? (
<> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<div className="text-2xl font-bold"> ) : (
{formatMetric(spending?.avg_cpf)} <>
</div> <div className="text-2xl font-bold">
<p className="text-xs text-muted-foreground"> {formatMetric(spending?.avg_cpm)}
{formatPercent(0 || 0, true)} от прошлого месяца </div>
</p> <p className="text-xs text-muted-foreground">
</> {formatPercent(0 || 0, true)} от прошлого месяца
)} </p>
</CardContent> </>
</Card> )}
</CardContent>
</Card>
</div>
<Card> <div className="grid gap-4 md:grid-cols-1">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <Card>
<CardTitle className="text-sm font-medium">Средний CPM</CardTitle> <CardHeader>
<BarChart3 className="h-4 w-4 text-muted-foreground" /> <CardTitle>Быстрый старт</CardTitle>
</CardHeader> <CardDescription>Начните с основных действий</CardDescription>
<CardContent> </CardHeader>
{loading ? ( <CardContent className="space-y-2">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <a
) : ( href="/purchases/new"
<> className="block text-sm text-primary hover:underline"
<div className="text-2xl font-bold"> >
{formatMetric(spending?.avg_cpm)} Создать новый закуп
</div> </a>
<p className="text-xs text-muted-foreground"> <a
{formatPercent(0 || 0, true)} от прошлого месяца href="/creatives/new"
</p> className="block text-sm text-primary hover:underline"
</> >
)} Создать креатив
</CardContent> </a>
</Card> <a
href="/external-channels/import"
className="block text-sm text-primary hover:underline"
>
Импортировать каналы
</a>
<a
href="/analytics"
className="block text-sm text-primary hover:underline"
>
Посмотреть аналитику
</a>
</CardContent>
</Card>
</div>
</div> </div>
)}
<div className="grid gap-4 md:grid-cols-1">
<Card>
<CardHeader>
<CardTitle>Быстрый старт</CardTitle>
<CardDescription>Начните с основных действий</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<a
href="/purchases/new"
className="block text-sm text-primary hover:underline"
>
Создать новый закуп
</a>
<a
href="/creatives/new"
className="block text-sm text-primary hover:underline"
>
Создать креатив
</a>
<a
href="/external-channels/import"
className="block text-sm text-primary hover:underline"
>
Импортировать каналы
</a>
<a
href="/analytics"
className="block text-sm text-primary hover:underline"
>
Посмотреть аналитику
</a>
</CardContent>
</Card>
</div>
</div>
</> </>
); );
} }

View File

@@ -47,7 +47,7 @@ export const TargetChannelSelector: React.FC = () => {
// Автоматический запуск polling при открытии диалога // Автоматический запуск polling при открытии диалога
useEffect(() => { useEffect(() => {
if (isAddDialogOpen && !isSuccess && initialChannelCount > 0) { if (isAddDialogOpen && !isSuccess && initialChannelCount >= 0) {
pollingIntervalRef.current = setInterval(() => { pollingIntervalRef.current = setInterval(() => {
checkForNewChannels(); checkForNewChannels();
}, 1000); }, 1000);