Files
tgex-frontend/components/layout/app-sidebar.tsx
2026-01-07 14:17:43 +03:00

973 lines
33 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";
// ============================================================================
// Application Sidebar
// ============================================================================
import * as React from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import {
BarChart3,
BookmarkCheck,
Building2,
CalendarClock,
ClipboardCheck,
ClipboardList,
Filter,
Folder,
Gauge,
KanbanSquare,
LayoutDashboard,
LineChart,
LifeBuoy,
ListChecks,
Megaphone,
MessageCircle,
MonitorPlay,
NotebookPen,
PlayCircle,
Settings,
ShoppingCart,
Sparkles,
Tv,
TrendingUp,
BookOpen,
ChevronRight,
X,
type LucideIcon,
} from "lucide-react";
import { type NavGuideState, NavMain } from "@/components/nav-main";
import { NavUser } from "@/components/nav-user";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail,
SidebarSeparator,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
useSidebar,
} from "@/components/ui/sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "@/components/auth/auth-provider";
import { useWorkspace } from "@/components/providers/workspace-provider";
import { buildRoute } from "@/lib/utils/constants";
import Image from "next/image";
import { DEMO_WORKSPACE_ID } from "@/lib/demo";
// ============================================================================
// Types
// ============================================================================
interface NavItem {
title: string;
url: string;
icon?: LucideIcon;
isActive?: boolean;
requiredPermission?: "projects_read" | "placements_read" | "analytics_read" | "admin_full";
items?: {
title: string;
url: string;
}[];
}
interface TrainingStep {
id: string;
title: string;
description: string;
navItemTitle: string;
expandedNavItems?: string[];
icon?: LucideIcon;
sections?: {
title: string;
description: string;
icon?: LucideIcon;
points?: string[];
}[];
points?: string[];
}
interface HelpItem {
title: string;
description: string;
icon: LucideIcon;
href?: string;
external?: boolean;
action?: () => void;
}
interface SupportLink {
title: string;
icon: LucideIcon;
href: string;
external?: boolean;
}
const TRAINING_STEPS: TrainingStep[] = [
{
id: "dashboard",
title: "Главная панель",
description:
"Это стартовая точка: отсюда видно общее состояние закупок, статус проектов и свежие уведомления. Сюда возвращаемся, чтобы понять, что делать дальше и какие задачи горят.",
navItemTitle: "Главная",
icon: LayoutDashboard,
sections: [
{
title: "Сводка показателей",
description:
"Карточки расхода, охвата и эффективности обновляются в режиме близи real-time. Это быстрый способ понять, всё ли идёт по плану.",
icon: Gauge,
points: [
"Подсветка указывает, где есть отклонения от плана или резкие изменения.",
"Нажмите на карточку, чтобы перейти к связанному отчёту или проекту.",
],
},
{
title: "Что происходит сейчас",
description:
"Лента событий показывает новые заявки, комментарии и дедлайны. Так команда не пропускает задачи.",
icon: CalendarClock,
points: [
"Фильтр по типу событий помогает отделить, например, комментарии от обновлений статусов.",
"Кнопка «Создать» рядом ведёт к быстрому созданию проектов и размещений.",
],
},
],
},
{
id: "projects",
title: "Проекты",
description:
"Управляйте рекламными активностями по каждому продукту или клиенту и контролируйте статусы задач.",
navItemTitle: "Проекты",
icon: KanbanSquare,
sections: [
{
title: "Карточки проектов",
description:
"Каждый проект знает свой бюджет, статус и ответственных. Это рабочее пространство конкретной кампании.",
icon: NotebookPen,
points: [
"Новые проекты создаются через кнопку «Добавить» — укажите клиента, цель и дедлайн.",
"Меняйте статусы (подготовка, в работе, завершён) по мере продвижения кампании.",
],
},
{
title: "Фильтры и поиск",
description:
"Фильтруйте проекты по менеджерам или тегам, чтобы быстро найти нужное направление.",
icon: Filter,
points: [
"Сохраняйте выбранный фильтр как «Избранный», чтобы открывать его в один клик.",
],
},
],
},
{
id: "channels",
title: "Каталог каналов",
description:
"Ищите площадки по темам и метрикам, собирайте избранные каналы и сравнивайте их перед закупкой.",
navItemTitle: "Каталог каналов",
icon: BookmarkCheck,
sections: [
{
title: "Поиск и фильтры",
description:
"Фильтруйте каналы по тематике, охвату и цене. Результаты можно сортировать и группировать.",
icon: Filter,
points: [
"Подборка фильтров сохраняется автоматически, чтобы вернуться к ней позже.",
],
},
{
title: "Работа с подборками",
description:
"Отмечайте интересные площадки, добавляйте их в планы закупов и сравнивайте показатели.",
icon: ListChecks,
points: [
"Одним кликом отправляйте канал в план или проект, чтобы не собирать всё заново.",
],
},
],
},
{
id: "purchase-plans",
title: "Планы закупов",
description:
"Это черновики кампаний. Собирайте каналы, фиксируйте ставки и согласовывайте бюджет до запуска.",
navItemTitle: "Планы закупов",
icon: ListChecks,
sections: [
{
title: "Конструктор кампаний",
description:
"Добавляйте каналы, задавайте даты и ставки. Система сама подсчитает бюджет и прогноз охвата.",
icon: NotebookPen,
points: [
"Используйте заметки, чтобы фиксировать договорённости с площадками.",
"Приглашайте коллег к плану — они увидят прогресс в реальном времени.",
],
},
{
title: "Согласование",
description:
"Когда план готов, его можно перевести в статус «Утверждён» и автоматически развернуть в размещения.",
icon: ClipboardCheck,
points: [
"История изменений сохранится и поможет восстановить контекст договорённостей.",
],
},
],
},
{
id: "placements",
title: "Размещения",
description:
"Здесь живут все закупки: от черновиков до опубликованных постов. Следите за статусом и результатами.",
navItemTitle: "Размещения",
icon: ClipboardCheck,
sections: [
{
title: "Линия жизни закупки",
description:
"Каждое размещение проходит статусы: черновик, согласовано, оплачено, опубликовано. Становится понятно, где застряли процессы.",
icon: CalendarClock,
points: [
"Обновляйте статусы и дедлайны — таблица подсветит просроченные задачи.",
],
},
{
title: "Фактические результаты",
description:
"После выхода поста вносите реальные просмотры, CTR и стоимость — аналитика подхватит данные автоматически.",
icon: Gauge,
points: [
"Добавляйте скриншоты и ссылки для архива — к ним всегда можно вернуться.",
],
},
],
},
{
id: "analytics",
title: "Аналитика",
description:
"Сводная аналитика показывает, что работает и куда уходят бюджеты. Не нужно собирать отчёты вручную.",
navItemTitle: "Аналитика",
expandedNavItems: ["Аналитика"],
icon: TrendingUp,
sections: [
{
title: "Срезы данных",
description:
"Выбирайте отчёт по проектам, каналам или креативам, чтобы увидеть вклад каждой части.",
icon: LineChart,
points: [
"Сравнивайте периоды и экспортируйте графики для презентаций.",
],
},
{
title: "Фильтры и сегменты",
description:
"Фильтр по менеджеру, периоду или типу кампании помогает отвечать на конкретные вопросы бизнеса.",
icon: Filter,
points: [
"Сохранённые сегменты экономят время: достаточно выбрать нужный набор критериев.",
],
},
],
},
];
// ============================================================================
// Component
// ============================================================================
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const params = useParams();
const workspaceId = params?.workspaceId as string;
const { user } = useAuth();
const {
workspaces,
currentWorkspace,
setCurrentWorkspace,
hasPermission,
isAdmin,
createWorkspace,
} = useWorkspace();
const { state: sidebarState } = useSidebar();
const isSidebarCollapsed = sidebarState === "collapsed";
const expandedLogoWidth = 182;
const expandedLogoHeight = 31;
const expandedLogoDimensions = React.useMemo(
() => ({
height: expandedLogoHeight,
width: expandedLogoWidth,
minWidth: expandedLogoWidth,
}),
[expandedLogoHeight, expandedLogoWidth]
);
const [showCreateDialog, setShowCreateDialog] = React.useState(false);
const [newWorkspaceName, setNewWorkspaceName] = React.useState("");
const [isCreating, setIsCreating] = React.useState(false);
const handleWorkspaceSelect = React.useCallback(
(workspaceId: string) => {
if (currentWorkspace?.id === workspaceId) return;
const workspace = workspaces.find((w) => w.id === workspaceId);
if (workspace) {
setCurrentWorkspace(workspace);
}
},
[currentWorkspace?.id, workspaces, setCurrentWorkspace]
);
// Build navigation items based on permissions
const navItems: NavItem[] = React.useMemo(() => {
if (!workspaceId) return [];
const items: NavItem[] = [];
// 1. Главная
items.push({
title: "Главная",
url: buildRoute.dashboard(workspaceId),
icon: LayoutDashboard,
isActive: true,
});
// 2. Проекты (всегда доступны)
items.push({
title: "Проекты",
url: buildRoute.projects(workspaceId),
icon: Tv,
});
// 3. Креативы (только с доступом к размещениям)
if (hasPermission("placements_read")) {
items.push({
title: "Креативы",
url: buildRoute.creatives(workspaceId),
icon: Folder,
requiredPermission: "placements_read",
});
}
// 4. Каталог каналов (всегда доступен)
items.push({
title: "Каталог каналов",
url: buildRoute.channels(workspaceId),
icon: Building2,
});
// 5. Планы закупов (правка названия)
if (hasPermission("placements_read")) {
items.push({
title: "Планы закупов",
url: buildRoute.purchasePlans(workspaceId),
icon: ClipboardList,
requiredPermission: "placements_read",
});
}
// 6. Размещения
if (hasPermission("placements_read")) {
items.push({
title: "Размещения",
url: buildRoute.placements(workspaceId),
icon: ShoppingCart,
requiredPermission: "placements_read",
});
}
// 7. Аналитика — только два подраздела
if (hasPermission("analytics_read")) {
items.push({
title: "Аналитика",
url: buildRoute.analytics(workspaceId),
icon: BarChart3,
requiredPermission: "analytics_read",
items: [
{
title: "По проектам",
url: buildRoute.analytics(workspaceId),
},
{
title: "По креативам",
url: buildRoute.analyticsCreatives(workspaceId),
},
],
});
}
// 8. Доступы (без подразделов, админ)
if (isAdmin) {
items.push({
title: "Доступы",
url: buildRoute.members(workspaceId),
icon: Settings,
requiredPermission: "admin_full",
});
}
return items;
}, [workspaceId, hasPermission, isAdmin]);
const availableTrainingSteps = React.useMemo(
() =>
TRAINING_STEPS.filter((step) =>
navItems.some((item) => item.title === step.navItemTitle)
),
[navItems]
);
const trainingStepsCount = availableTrainingSteps.length;
const [isTrainingOpen, setIsTrainingOpen] = React.useState(false);
const [trainingStepIndex, setTrainingStepIndex] = React.useState(0);
const safeTrainingStepIndex =
trainingStepsCount > 0
? Math.min(trainingStepIndex, trainingStepsCount - 1)
: 0;
const currentTrainingStep =
trainingStepsCount > 0
? availableTrainingSteps[safeTrainingStepIndex]
: undefined;
React.useEffect(() => {
if (trainingStepsCount === 0) {
setIsTrainingOpen(false);
setTrainingStepIndex(0);
return;
}
if (trainingStepIndex > trainingStepsCount - 1) {
setTrainingStepIndex(0);
}
}, [trainingStepIndex, trainingStepsCount]);
const openTraining = React.useCallback(() => {
if (!trainingStepsCount) return;
setTrainingStepIndex(0);
setIsTrainingOpen(true);
}, [trainingStepsCount]);
const closeTraining = React.useCallback(() => {
setIsTrainingOpen(false);
}, []);
const goToNextTrainingStep = React.useCallback(() => {
setTrainingStepIndex((prev) =>
Math.min(prev + 1, Math.max(trainingStepsCount - 1, 0))
);
}, [trainingStepsCount]);
const goToPrevTrainingStep = React.useCallback(() => {
setTrainingStepIndex((prev) => Math.max(prev - 1, 0));
}, []);
const navGuideState = React.useMemo<NavGuideState | undefined>(() => {
if (!isTrainingOpen || !currentTrainingStep) return undefined;
return {
highlightedItem: currentTrainingStep.navItemTitle,
expandedItems: currentTrainingStep.expandedNavItems,
};
}, [isTrainingOpen, currentTrainingStep]);
const isTrainingVisible = isTrainingOpen && Boolean(currentTrainingStep);
const supportLinks = React.useMemo<SupportLink[]>(
() => [
{
title: "Наш канал",
href: "https://t.me/tgex_news",
icon: Megaphone,
external: true,
},
{
title: "Поддержка",
href: "https://t.me/smartpost_help_bot",
icon: MessageCircle,
external: true,
},
{
title: "База знаний",
href: "https://docs.smartpost.ru",
icon: BookOpen,
external: true,
},
],
[]
);
const extraHelpItems = React.useMemo<HelpItem[]>(
() => [
{
title: "Демо дашборд",
description: "Посмотрите пример рабочего пространства со всеми разделами.",
href: buildRoute.dashboard(DEMO_WORKSPACE_ID),
icon: Sparkles,
},
{
title: "Открыть обучение",
description: "Запустите интерактивный тур по основным разделам.",
action: openTraining,
icon: MonitorPlay,
},
],
[openTraining]
);
const renderExtraHelpItem = React.useCallback((item: HelpItem) => {
const content = (
<>
<item.icon className="mt-1.5 size-3 shrink-0 text-muted-foreground" />
<p className="text-xs font-light mt-1.5">{item.title}</p>
</>
);
if (item.action) {
return (
<button
type="button"
onClick={() => item.action?.()}
className="flex w-full items-start gap-3 text-left"
>
{content}
</button>
);
}
if (item.href) {
if (item.external) {
return (
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-start gap-3 text-left"
>
{content}
</a>
);
}
return (
<Link href={item.href} className="flex w-full items-start gap-3 text-left">
{content}
</Link>
);
}
return (
<div className="flex w-full items-start gap-3 text-left">{content}</div>
);
}, []);
const handleCreateWorkspace = async () => {
if (!newWorkspaceName.trim()) return;
try {
setIsCreating(true);
const workspace = await createWorkspace(newWorkspaceName.trim());
setShowCreateDialog(false);
setNewWorkspaceName("");
setCurrentWorkspace(workspace);
} catch (err) {
console.error("Failed to create workspace:", err);
} finally {
setIsCreating(false);
}
};
return (
<>
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
size="lg"
className="cursor-default border border-transparent transition-none bg-transparent hover:bg-transparent active:bg-transparent"
>
{isSidebarCollapsed ? (
<div className="flex aspect-square size-8 items-center justify-center">
<Image
src="/logo.webp"
width={64}
height={64}
alt="Smartpost"
className="rounded-lg"
/>
</div>
) : (
<div
className="flex items-center overflow-hidden"
style={expandedLogoDimensions}
>
<Image
src="/logo_full_dark.webp"
width={expandedLogoWidth}
height={expandedLogoHeight}
alt="Smartpost"
className="shrink-0 dark:hidden"
style={expandedLogoDimensions}
/>
<Image
src="/logo_full_white_2.png"
width={expandedLogoWidth}
height={expandedLogoHeight}
alt="Smartpost"
className="hidden shrink-0 dark:block"
style={expandedLogoDimensions}
/>
</div>
)}
{/* <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">Smartpost</span>
<span className="truncate text-xs text-muted-foreground">
Управление закупками
</span>
</div> */}
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={navItems} guideState={navGuideState} />
</SidebarContent>
<SidebarFooter>
<SidebarGroup className="px-0">
<SidebarGroupContent>
<SidebarMenu>
{supportLinks.map((link) => (
<SidebarMenuItem key={link.title}>
<SidebarMenuButton
asChild
size="sm"
tooltip={isSidebarCollapsed ? link.title : undefined}
>
{link.external ? (
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<link.icon className="size-4" />
<span>{link.title}</span>
</a>
) : (
<Link href={link.href} className="flex items-center gap-2">
<link.icon className="size-4" />
<span>{link.title}</span>
</Link>
)}
</SidebarMenuButton>
</SidebarMenuItem>
))}
{isSidebarCollapsed ? (
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="sm"
tooltip="Помощь"
className="justify-between"
>
<div className="flex items-center gap-2">
<LifeBuoy className="size-4" />
<span>Помощь</span>
</div>
<ChevronRight className="size-4 opacity-60" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="right"
align="start"
className="min-w-[280px] space-y-1"
>
{extraHelpItems.map((item) => (
<DropdownMenuItem key={item.title} asChild>
{renderExtraHelpItem(item)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
) : (
<Collapsible defaultOpen asChild>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
size="sm"
className="justify-between"
>
<div className="flex items-center gap-2">
<LifeBuoy className="size-4" />
<span>Помощь</span>
</div>
<ChevronRight className="size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{extraHelpItems.map((item) => (
<SidebarMenuSubItem key={item.title}>
<SidebarMenuSubButton asChild className="">
{renderExtraHelpItem(item)}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarSeparator />
{/* User */}
{user && (
<NavUser
user={{
name: user.username || `User ${user.telegram_id}`,
email: user.username
? `@${user.username}`
: `ID: ${user.telegram_id}`,
avatar: user.username
? `https://ui-avatars.com/api/?name=${user.username}`
: `https://ui-avatars.com/api/?name=User`,
}}
workspaceSwitcher={{
workspaces,
currentWorkspaceId: currentWorkspace?.id,
onSelect: (workspace) => handleWorkspaceSelect(workspace.id),
onCreate: () => setShowCreateDialog(true),
}}
/>
)}
</SidebarFooter>
<SidebarRail />
</Sidebar>
{isTrainingVisible && (
<div className="fixed inset-0 z-30 bg-transparent" aria-hidden="true" />
)}
<TrainingGuidePanel
open={isTrainingVisible}
step={currentTrainingStep}
stepIndex={safeTrainingStepIndex}
totalSteps={trainingStepsCount}
onClose={closeTraining}
onNext={goToNextTrainingStep}
onPrev={goToPrevTrainingStep}
/>
{/* Create Workspace Dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Создать воркспейс</DialogTitle>
<DialogDescription>
Воркспейс это изолированное пространство для вашей команды
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="workspace-name">Название</Label>
<Input
id="workspace-name"
placeholder="Например: Моя команда"
value={newWorkspaceName}
onChange={(e) => setNewWorkspaceName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleCreateWorkspace();
}
}}
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowCreateDialog(false)}
>
Отмена
</Button>
<Button
onClick={handleCreateWorkspace}
disabled={!newWorkspaceName.trim() || isCreating}
>
{isCreating ? "Создание..." : "Создать"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
interface TrainingGuidePanelProps {
open: boolean;
step?: TrainingStep;
stepIndex: number;
totalSteps: number;
onPrev: () => void;
onNext: () => void;
onClose: () => void;
}
function TrainingGuidePanel({
open,
step,
stepIndex,
totalSteps,
onPrev,
onNext,
onClose,
}: TrainingGuidePanelProps) {
if (!open || !step) {
return null;
}
return (
<div
className="fixed inset-y-0 right-0 z-40 flex w-full justify-end"
role="dialog"
aria-modal="true"
aria-label="Обучение платформе"
>
<div className="flex h-full w-full max-w-md flex-col gap-6 border-l bg-background/95 p-6 shadow-2xl backdrop-blur">
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2 text-xs font-semibold uppercase text-muted-foreground">
<PlayCircle className="size-4 text-primary" />
Обучение
</div>
<p className="text-xs text-muted-foreground">
Шаг {stepIndex + 1} из {totalSteps}
</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={onClose}
aria-label="Закрыть обучение"
>
<X className="size-4" />
</Button>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3">
{step.icon ? (
<div className="rounded-full bg-primary/10 p-2 text-primary">
<step.icon className="size-5" />
</div>
) : null}
<h3 className="text-2xl font-semibold">{step.title}</h3>
</div>
<p className="text-sm text-muted-foreground">{step.description}</p>
</div>
{step.sections?.length ? (
<div className="space-y-4">
{step.sections.map((section) => (
<div
key={section.title}
className="rounded-lg border border-border/70 bg-muted/20 p-4"
>
<div className="flex items-start gap-3">
{section.icon ? (
<div className="rounded-md bg-primary/10 p-2 text-primary">
<section.icon className="size-4" />
</div>
) : null}
<div>
<p className="text-sm font-semibold">{section.title}</p>
<p className="text-xs text-muted-foreground">
{section.description}
</p>
</div>
</div>
{section.points?.length ? (
<ul className="mt-3 space-y-2 text-sm text-muted-foreground">
{section.points.map((point) => (
<li
key={point}
className="flex items-start gap-2 rounded-md bg-background/80 p-2"
>
<span className="mt-1 h-1.5 w-1.5 rounded-full bg-primary" />
<span>{point}</span>
</li>
))}
</ul>
) : null}
</div>
))}
</div>
) : step.points?.length ? (
<ul className="space-y-3 text-sm text-muted-foreground">
{step.points.map((point) => (
<li
key={point}
className="rounded-md border border-border/60 bg-muted/40 p-3"
>
{point}
</li>
))}
</ul>
) : null}
<div className="mt-auto flex gap-2 pt-2">
<Button variant="outline" onClick={onPrev} disabled={stepIndex === 0}>
Назад
</Button>
{stepIndex >= totalSteps - 1 ? (
<Button className="flex-1" onClick={onClose}>
Завершить
</Button>
) : (
<Button className="flex-1" onClick={onNext}>
Дальше
</Button>
)}
</div>
</div>
</div>
);
}