import { LayoutDashboard, Server, ServerCog, Building2, Wallet, CreditCard, Coins, ChartColumnBig, ChartBar, Settings, RefreshCwIcon, FolderKanbanIcon, HistoryIcon, } from 'lucide-react' import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, } from '@cfdm/ui/components/sidebar' import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@cfdm/ui/components/breadcrumb' import { Separator } from '@cfdm/ui/components/separator' import { Badge } from '@cfdm/ui/components/badge' import { Link, useRouterState } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' import { useState, type ReactNode } from 'react' import { ModeToggle } from '@/components/mode-toggle' import { GlobalSearch, GlobalSearchTrigger, useGlobalSearchHotkey } from '@/components/global-search' import { dashboardStatsQueryOptions } from '@/queries/dashboard' import { formatRelativeSyncTime } from '@/lib/sync-format' import { TruncatedText } from '@/components/truncated-text' interface NavItem { to: string label: string icon: typeof LayoutDashboard badge?: number } interface NavGroup { label: string items: NavItem[] } const NAV_GROUPS: NavGroup[] = [ { label: 'Обзор', items: [{ to: '/dashboard', label: 'Дашборд', icon: LayoutDashboard }], }, { label: 'Инфраструктура', items: [ { to: '/vps', label: 'VPS', icon: Server }, { to: '/tariffs', label: 'Активные тарифы', icon: ServerCog }, { to: '/providers', label: 'Хостеры', icon: Building2 }, { to: '/accounts', label: 'Аккаунты хостеров', icon: Wallet }, { to: '/projects', label: 'Проекты', icon: FolderKanbanIcon }, ], }, { label: 'Финансы', items: [ { to: '/payments', label: 'Платежи', icon: CreditCard }, { to: '/balance', label: 'Баланс и списания', icon: Coins }, ], }, { label: 'Аналитика', items: [ { to: '/reports', label: 'Отчёты', icon: ChartColumnBig }, { to: '/resources', label: 'Ресурсы', icon: ChartBar }, { to: '/renewals', label: 'Продления', icon: RefreshCwIcon }, ], }, { label: 'Система', items: [ { to: '/sync-journal', label: 'Журнал синка', icon: HistoryIcon }, { to: '/audit', label: 'Журнал изменений', icon: HistoryIcon }, { to: '/settings', label: 'Настройки', icon: Settings }, ], }, ] const ALL_NAV_ITEMS = NAV_GROUPS.flatMap((g) => g.items) const ROUTE_LABELS: Record = Object.fromEntries( ALL_NAV_ITEMS.map((i) => [i.to, i.label]), ) const PARENT_ROUTE: Record = { '/vps': '/dashboard', '/tariffs': '/dashboard', '/providers': '/dashboard', '/accounts': '/dashboard', '/projects': '/dashboard', '/payments': '/dashboard', '/balance': '/dashboard', '/reports': '/dashboard', '/resources': '/dashboard', '/renewals': '/dashboard', '/sync-journal': '/settings', '/audit': '/settings', } export function AppShell({ children }: { children: ReactNode }) { const pathname = useRouterState({ select: (s) => s.location.pathname }) const [searchOpen, setSearchOpen] = useState(false) useGlobalSearchHotkey(() => setSearchOpen(true)) const activeItem = ALL_NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) ?? ALL_NAV_ITEMS[0] const parentTo = PARENT_ROUTE[activeItem.to] const parentLabel = parentTo ? ROUTE_LABELS[parentTo] : null const { data: stats } = useQuery(dashboardStatsQueryOptions()) const navGroups: NavGroup[] = NAV_GROUPS.map((group) => ({ ...group, items: group.items.map((item) => { if (item.to === '/dashboard' && stats?.issuesCount) { return { ...item, badge: stats.issuesCount } } return item }), })) return ( }>
VPS Tracker Учёт виртуальных серверов
{navGroups.map((group) => ( {group.label} {group.items.map((item) => { const Icon = item.icon const isActive = pathname === item.to || pathname.startsWith(`${item.to}/`) return ( } isActive={isActive} tooltip={item.label} > {item.label} {item.badge ? ( {item.badge} ) : null} ) })} ))} } tooltip="Настройки"> Синк: {formatRelativeSyncTime(stats?.lastGlobalSyncAt)} {stats?.staleSyncAccountCount ? ( {stats.staleSyncAccountCount} ) : null}
{parentLabel && parentTo ? ( <> }>{parentLabel} ) : null} {ROUTE_LABELS[activeItem.to] ?? ''}
setSearchOpen(true)} /> {stats?.issuesCount ? ( {stats.issuesCount} проблем ) : null}
{children}
) }