"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; icon?: LucideIcon; }[]; } 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) { const params = useParams(); const workspaceId = params?.workspaceId as string; const { user } = useAuth(); const { workspaces, currentWorkspace, setCurrentWorkspace, hasPermission, isAdmin, createWorkspace, isDemoMode, } = 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), icon: Tv, }, { title: "По креативам", url: buildRoute.analyticsCreatives(workspaceId), icon: Folder, }, ], }); } // 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(() => { if (!isTrainingOpen || !currentTrainingStep) return undefined; return { highlightedItem: currentTrainingStep.navItemTitle, expandedItems: currentTrainingStep.expandedNavItems, }; }, [isTrainingOpen, currentTrainingStep]); const isTrainingVisible = isTrainingOpen && Boolean(currentTrainingStep); const supportLinks = React.useMemo( () => [ { 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( () => [ { 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.title}

); if (item.action) { return ( ); } if (item.href) { if (item.external) { return ( {content} ); } return ( {content} ); } return (
{content}
); }, []); 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 ( <> {isSidebarCollapsed ? (
Smartpost
) : (
Smartpost Smartpost
)} {/*
Smartpost Управление закупками
*/}
{supportLinks.map((link) => ( {link.external ? ( {link.title} ) : ( {link.title} )} ))} {isSidebarCollapsed ? (
Помощь
{extraHelpItems.map((item) => ( {renderExtraHelpItem(item)} ))}
) : (
Помощь
{extraHelpItems.map((item) => ( {renderExtraHelpItem(item)} ))}
)}
{/* User */} {user && ( handleWorkspaceSelect(workspace.id), onCreate: isDemoMode ? undefined : () => setShowCreateDialog(true), }} /> )}
{isTrainingVisible && (