754 lines
29 KiB
TypeScript
754 lines
29 KiB
TypeScript
"use client";
|
||
|
||
// ============================================================================
|
||
// Projects Page (Target Channels) - Enhanced with Statistics
|
||
// ============================================================================
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { useParams } from "next/navigation";
|
||
import Link from "next/link";
|
||
import {
|
||
Loader2,
|
||
Tv,
|
||
ExternalLink,
|
||
Grid,
|
||
List,
|
||
TrendingUp,
|
||
Users,
|
||
Eye,
|
||
ShoppingCart,
|
||
Info,
|
||
Bot,
|
||
Settings,
|
||
Folder,
|
||
Archive,
|
||
Trash2,
|
||
} from "lucide-react";
|
||
import { DashboardHeader } from "@/components/layout/dashboard-header";
|
||
import { useWorkspace } from "@/components/providers/workspace-provider";
|
||
import { demoAwareAnalyticsApi, demoAwareProjectsApi } from "@/lib/demo";
|
||
import { projectsApi } from "@/lib/api";
|
||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import {
|
||
AlertDialog,
|
||
AlertDialogAction,
|
||
AlertDialogCancel,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogFooter,
|
||
AlertDialogHeader,
|
||
AlertDialogTitle,
|
||
} from "@/components/ui/alert-dialog";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Label } from "@/components/ui/label";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/components/ui/table";
|
||
import {
|
||
Tooltip,
|
||
TooltipContent,
|
||
TooltipProvider,
|
||
TooltipTrigger,
|
||
} from "@/components/ui/tooltip";
|
||
import { BOT_USERNAME } from "@/lib/utils/constants";
|
||
import type { Project, SpendingAnalytics } from "@/lib/types/api";
|
||
|
||
// ============================================================================
|
||
// Types
|
||
// ============================================================================
|
||
|
||
interface ProjectWithStats extends Project {
|
||
stats?: {
|
||
total_cost: number;
|
||
total_subscriptions: number;
|
||
total_views: number;
|
||
avg_cpf: number | null;
|
||
};
|
||
}
|
||
|
||
// ============================================================================
|
||
// Helpers
|
||
// ============================================================================
|
||
|
||
const formatNumber = (value: number | null | undefined): string => {
|
||
if (value === null || value === undefined) return "—";
|
||
return new Intl.NumberFormat("ru-RU").format(value);
|
||
};
|
||
|
||
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 ProjectsPage() {
|
||
const params = useParams();
|
||
const workspaceId = params?.workspaceId as string;
|
||
const { projects, isLoadingProjects, currentWorkspace, refreshProjects } = useWorkspace();
|
||
|
||
const [projectsWithStats, setProjectsWithStats] = useState<ProjectWithStats[]>([]);
|
||
const [loadingStats, setLoadingStats] = useState(false);
|
||
const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
|
||
const [settingsProject, setSettingsProject] = useState<Project | null>(null);
|
||
const [inviteLinkType, setInviteLinkType] = useState<"public" | "approval">("public");
|
||
const [isUpdatingSettings, setIsUpdatingSettings] = useState(false);
|
||
const [isArchiving, setIsArchiving] = useState(false);
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||
const [deleteConfirmationText, setDeleteConfirmationText] = useState("");
|
||
const { isDemoMode } = useWorkspace();
|
||
|
||
const handleOpenSettings = (project: Project) => {
|
||
setSettingsProject(project);
|
||
setInviteLinkType(project.purchase_invite_type_default || "public");
|
||
};
|
||
|
||
const handleSaveSettings = async () => {
|
||
if (!settingsProject) return;
|
||
|
||
try {
|
||
setIsUpdatingSettings(true);
|
||
const api = isDemoMode ? demoAwareProjectsApi : projectsApi;
|
||
await api.updateInviteLinkType(workspaceId, settingsProject.id, {
|
||
purchase_invite_type_default: inviteLinkType,
|
||
});
|
||
await refreshProjects();
|
||
setSettingsProject(null);
|
||
} catch (err) {
|
||
console.error("Failed to update settings:", err);
|
||
} finally {
|
||
setIsUpdatingSettings(false);
|
||
}
|
||
};
|
||
|
||
const handleArchive = async () => {
|
||
if (!settingsProject) return;
|
||
|
||
try {
|
||
setIsArchiving(true);
|
||
const api = isDemoMode ? demoAwareProjectsApi : projectsApi;
|
||
await api.archive(workspaceId, settingsProject.id);
|
||
await refreshProjects();
|
||
setSettingsProject(null);
|
||
} catch (err) {
|
||
console.error("Failed to archive project:", err);
|
||
} finally {
|
||
setIsArchiving(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async () => {
|
||
if (!settingsProject) return;
|
||
|
||
try {
|
||
setIsDeleting(true);
|
||
const api = isDemoMode ? demoAwareProjectsApi : projectsApi;
|
||
await api.delete(workspaceId, settingsProject.id);
|
||
await refreshProjects();
|
||
setSettingsProject(null);
|
||
setShowDeleteDialog(false);
|
||
setDeleteConfirmationText("");
|
||
} catch (err) {
|
||
console.error("Failed to delete project:", err);
|
||
} finally {
|
||
setIsDeleting(false);
|
||
}
|
||
};
|
||
|
||
const handleOpenDeleteDialog = () => {
|
||
setShowDeleteDialog(true);
|
||
};
|
||
|
||
const handleCloseDeleteDialog = () => {
|
||
setShowDeleteDialog(false);
|
||
setDeleteConfirmationText("");
|
||
};
|
||
|
||
const getChannelAvatar = (project: Project) => {
|
||
// Telegram channel avatar URL format
|
||
if (project.username) {
|
||
return `https://t.me/i/userpic/320/${project.username}.jpg`;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// Load statistics for each project
|
||
useEffect(() => {
|
||
if (projects.length === 0) {
|
||
setProjectsWithStats([]);
|
||
return;
|
||
}
|
||
|
||
const loadStats = async () => {
|
||
setLoadingStats(true);
|
||
try {
|
||
const statsPromises = projects.map(async (project) => {
|
||
try {
|
||
const spending = await demoAwareAnalyticsApi.spending(workspaceId, {
|
||
project_id: project.id,
|
||
});
|
||
return {
|
||
...project,
|
||
stats: {
|
||
total_cost: spending.total_cost,
|
||
total_subscriptions: spending.total_subscriptions,
|
||
total_views: spending.total_views,
|
||
avg_cpf:
|
||
spending.total_subscriptions > 0
|
||
? spending.total_cost / spending.total_subscriptions
|
||
: null,
|
||
},
|
||
};
|
||
} catch {
|
||
return { ...project, stats: undefined };
|
||
}
|
||
});
|
||
|
||
const results = await Promise.all(statsPromises);
|
||
setProjectsWithStats(results);
|
||
} catch (err) {
|
||
console.error("Failed to load project stats:", err);
|
||
setProjectsWithStats(projects);
|
||
} finally {
|
||
setLoadingStats(false);
|
||
}
|
||
};
|
||
|
||
loadStats();
|
||
}, [projects, workspaceId]);
|
||
|
||
const isLoading = isLoadingProjects || loadingStats;
|
||
|
||
return (
|
||
<TooltipProvider>
|
||
<DashboardHeader
|
||
breadcrumbs={[
|
||
{ label: "Главная", href: `/dashboard/${currentWorkspace?.id}` },
|
||
{ 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">
|
||
Telegram каналы, которые вы продвигаете
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2 border rounded-lg p-1">
|
||
<Button
|
||
variant={viewMode === "grid" ? "secondary" : "ghost"}
|
||
size="sm"
|
||
onClick={() => setViewMode("grid")}
|
||
aria-label="Плиточный вид"
|
||
>
|
||
<Grid className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant={viewMode === "table" ? "secondary" : "ghost"}
|
||
size="sm"
|
||
onClick={() => setViewMode("table")}
|
||
aria-label="Табличный вид"
|
||
>
|
||
<List className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Alert>
|
||
<Tv className="h-4 w-4" />
|
||
<AlertDescription className="flex items-center justify-between flex-wrap gap-3">
|
||
<span>
|
||
Чтобы добавить новый проект, добавьте бота{" "}
|
||
<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>{" "}
|
||
администратором в ваш Telegram канал.
|
||
</span>
|
||
<Button
|
||
asChild
|
||
size="sm"
|
||
className="shrink-0"
|
||
>
|
||
<a
|
||
href={`tg://resolve?domain=${BOT_USERNAME}&startchannel&admin=invite_users+post_messages+edit_messages+delete_messages`}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
>
|
||
<Bot className="h-4 w-4 mr-2" />
|
||
Добавить бота
|
||
</a>
|
||
</Button>
|
||
</AlertDescription>
|
||
</Alert>
|
||
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center py-12">
|
||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||
</div>
|
||
) : projectsWithStats.length === 0 ? (
|
||
<Card>
|
||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||
<Tv className="h-12 w-12 text-muted-foreground mb-4" />
|
||
<h3 className="text-lg font-semibold mb-2">Нет проектов</h3>
|
||
<p className="text-muted-foreground text-center max-w-md">
|
||
Добавьте бота администратором в ваш Telegram канал, и он
|
||
автоматически появится здесь как проект.
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
) : viewMode === "table" ? (
|
||
// Table View
|
||
<Card>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>Канал</TableHead>
|
||
<TableHead>Статус</TableHead>
|
||
<TableHead className="text-right">
|
||
<div className="flex items-center justify-end gap-1">
|
||
Подписчиков
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
Количество подписчиков в канале
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</div>
|
||
</TableHead>
|
||
<TableHead className="text-right">
|
||
<div className="flex items-center justify-end gap-1">
|
||
Привлечено подписчиков
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
Привлеченные подписчики по пригласительным ссылкам
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</div>
|
||
</TableHead>
|
||
<TableHead className="text-right">
|
||
<div className="flex items-center justify-end gap-1">
|
||
Суммарный охват
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
Суммарные просмотры рекламных постов
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</div>
|
||
</TableHead>
|
||
<TableHead className="text-right">
|
||
<div className="flex items-center justify-end gap-1">
|
||
CPF
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
Cost Per Follower — средняя стоимость подписчика
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</div>
|
||
</TableHead>
|
||
<TableHead className="w-[200px]"></TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{projectsWithStats.map((project) => (
|
||
<TableRow key={project.id}>
|
||
<TableCell>
|
||
<div className="flex items-center gap-3">
|
||
<Avatar className="h-9 w-9">
|
||
<AvatarImage src={getChannelAvatar(project) || undefined} alt={project.title} />
|
||
<AvatarFallback className="rounded-lg bg-primary/10">
|
||
<Tv className="h-4 w-4 text-primary" />
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div>
|
||
<div className="font-medium">{project.title}</div>
|
||
{project.username && (
|
||
<a
|
||
href={`https://t.me/${project.username}`}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="text-xs text-muted-foreground hover:underline inline-flex items-center gap-0.5"
|
||
>
|
||
@{project.username}
|
||
<ExternalLink className="h-2.5 w-2.5" />
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Badge
|
||
variant={project.status === "active" ? "default" : "secondary"}
|
||
>
|
||
{project.status === "active"
|
||
? "Активен"
|
||
: project.status === "inactive"
|
||
? "Неактивен"
|
||
: "Архив"}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
{formatNumber(project.subscribers_count)}
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
{formatNumber(project.stats?.total_subscriptions)}
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
{formatNumber(project.stats?.total_views)}
|
||
</TableCell>
|
||
<TableCell className="text-right">
|
||
{formatCurrency(project.stats?.avg_cpf)}
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex gap-1 flex-wrap">
|
||
<Button variant="outline" size="sm" asChild>
|
||
<Link
|
||
href={`/dashboard/${currentWorkspace?.id}/purchase-plans/${project.id}`}
|
||
>
|
||
План закупов
|
||
</Link>
|
||
</Button>
|
||
<Button variant="outline" size="sm" asChild>
|
||
<Link
|
||
href={`/dashboard/${currentWorkspace?.id}/creatives?project_id=${project.id}`}
|
||
>
|
||
<Folder className="h-3 w-3 mr-1" />
|
||
Креативы
|
||
</Link>
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleOpenSettings(project)}
|
||
>
|
||
<Settings className="h-3 w-3 mr-1" />
|
||
Настройки
|
||
</Button>
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</Card>
|
||
) : (
|
||
// Grid View
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{projectsWithStats.map((project) => (
|
||
<Card key={project.id}>
|
||
<CardHeader>
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<Avatar className="h-10 w-10">
|
||
<AvatarImage src={getChannelAvatar(project) || undefined} alt={project.title} />
|
||
<AvatarFallback className="rounded-lg bg-primary/10">
|
||
<Tv className="h-5 w-5 text-primary" />
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div>
|
||
<CardTitle className="text-base">{project.title}</CardTitle>
|
||
{project.username && (
|
||
<CardDescription>
|
||
<a
|
||
href={`https://t.me/${project.username}`}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="hover:underline inline-flex items-center gap-0.5"
|
||
>
|
||
@{project.username}
|
||
<ExternalLink className="h-2.5 w-2.5" />
|
||
</a>
|
||
</CardDescription>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<Badge
|
||
variant={project.status === "active" ? "default" : "secondary"}
|
||
>
|
||
{project.status === "active"
|
||
? "Активен"
|
||
: project.status === "inactive"
|
||
? "Неактивен"
|
||
: "Архив"}
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{/* Stats Grid */}
|
||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||
{project.subscribers_count !== undefined && (
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-4 w-4 text-muted-foreground" />
|
||
<div>
|
||
<div className="font-medium">
|
||
{formatNumber(project.subscribers_count)}
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
Подписчиков
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{project.stats && (
|
||
<>
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-4 w-4 text-muted-foreground" />
|
||
<div>
|
||
<div className="font-medium">
|
||
{formatNumber(project.stats.total_subscriptions)}
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
Привлечено подписчиков
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||
<div>
|
||
<div className="font-medium">
|
||
{formatNumber(project.stats.total_views)}
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
Суммарный охват
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
||
<div>
|
||
<div className="font-medium">
|
||
{formatCurrency(project.stats.avg_cpf)}
|
||
</div>
|
||
<div className="text-xs text-muted-foreground">
|
||
CPF
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex gap-2 flex-wrap">
|
||
<Button variant="outline" size="sm" asChild>
|
||
<Link
|
||
href={`/dashboard/${currentWorkspace?.id}/purchase-plans/${project.id}`}
|
||
>
|
||
План закупов
|
||
</Link>
|
||
</Button>
|
||
<Button variant="outline" size="sm" asChild>
|
||
<Link
|
||
href={`/dashboard/${currentWorkspace?.id}/creatives?project_id=${project.id}`}
|
||
>
|
||
<Folder className="h-4 w-4 mr-1" />
|
||
Креативы
|
||
</Link>
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleOpenSettings(project)}
|
||
>
|
||
<Settings className="h-4 w-4 mr-1" />
|
||
Настройки
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Settings Dialog */}
|
||
<Dialog open={!!settingsProject} onOpenChange={(open) => !open && setSettingsProject(null)}>
|
||
<DialogContent className="sm:max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle>Настройки проекта</DialogTitle>
|
||
<DialogDescription>
|
||
{settingsProject?.title}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="invite-link-type">Тип ссылки по умолчанию</Label>
|
||
<Select
|
||
value={inviteLinkType}
|
||
onValueChange={(value) => setInviteLinkType(value as "public" | "approval")}
|
||
>
|
||
<SelectTrigger id="invite-link-type">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="public">Открытая</SelectItem>
|
||
<SelectItem value="approval">С заявками</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<p className="text-xs text-muted-foreground">
|
||
Тип пригласительной ссылки, который будет использоваться по умолчанию при создании размещений для этого проекта
|
||
</p>
|
||
</div>
|
||
|
||
{/* Danger Zone */}
|
||
<div className="space-y-3 pt-4 border-t">
|
||
<div className="space-y-2">
|
||
<h4 className="text-sm font-semibold text-destructive">Опасная зона</h4>
|
||
<div className="flex flex-col gap-2">
|
||
{settingsProject?.status !== "archived" && (
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleArchive}
|
||
disabled={isArchiving || isDeleting || isUpdatingSettings}
|
||
className="w-full justify-start"
|
||
>
|
||
{isArchiving ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||
Архивирование...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Archive className="h-4 w-4 mr-2" />
|
||
Архивировать проект
|
||
</>
|
||
)}
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="destructive"
|
||
onClick={handleOpenDeleteDialog}
|
||
disabled={isArchiving || isDeleting || isUpdatingSettings}
|
||
className="w-full justify-start"
|
||
>
|
||
<Trash2 className="h-4 w-4 mr-2" />
|
||
Удалить проект
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setSettingsProject(null)}
|
||
>
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
onClick={handleSaveSettings}
|
||
disabled={isUpdatingSettings || isArchiving || isDeleting}
|
||
>
|
||
{isUpdatingSettings ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||
Сохранение...
|
||
</>
|
||
) : (
|
||
"Сохранить"
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Delete Confirmation Dialog */}
|
||
<AlertDialog open={showDeleteDialog} onOpenChange={handleCloseDeleteDialog}>
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>Удалить проект "{settingsProject?.title}"?</AlertDialogTitle>
|
||
<AlertDialogDescription>
|
||
Это действие необратимо. Все данные проекта будут удалены, включая креативы, размещения и аналитику.
|
||
<br /><br />
|
||
Для подтверждения введите <strong>Удалить {settingsProject?.title}</strong> ниже:
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<div className="space-y-4 py-4">
|
||
<Input
|
||
placeholder={`Удалить ${settingsProject?.title}`}
|
||
value={deleteConfirmationText}
|
||
onChange={(e) => setDeleteConfirmationText(e.target.value)}
|
||
className="mt-4"
|
||
/>
|
||
</div>
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel onClick={handleCloseDeleteDialog}>
|
||
Отмена
|
||
</AlertDialogCancel>
|
||
<AlertDialogAction
|
||
onClick={handleDelete}
|
||
disabled={isDeleting || deleteConfirmationText !== `Удалить ${settingsProject?.title}`}
|
||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||
>
|
||
{isDeleting ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||
Удаление...
|
||
</>
|
||
) : (
|
||
"Удалить"
|
||
)}
|
||
</AlertDialogAction>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
</div>
|
||
</TooltipProvider>
|
||
);
|
||
}
|