import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { LayoutDashboard, Inbox, Users, Calendar, CalendarDays, AlertTriangle, Star, Ban, Ticket, CreditCard, Shield, FileSearch, Activity, ChevronLeft, ChevronRight, LogOut, User, Search, Rows3, Check, Languages, } from 'lucide-react'; import { useAuthStore } from '@/store/authStore'; import { useAdminWebSocket } from '@/hooks/useAdminWebSocket'; import { useUpdateProfile } from '@/hooks/useProfile'; import { filterNavGroupsByRole, getMenuSelectedKey, canReceiveWsNotification, NAV_GROUPS, type NavItem, } from '@/config/adminAccess'; import { notify } from '@/lib/notify'; import { cn } from '@/lib/utils'; import { getTableDensity, toggleTableDensity, type TableDensity } from '@/lib/density'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { ErrorBoundary } from '@/components/ErrorBoundary'; import { CommandPalette } from '@/components/CommandPalette'; import { useCommandPaletteHotkey } from '@/hooks/useCommandPaletteHotkey'; import { HeaderNodeMetrics } from '@/components/HeaderNodeMetrics'; import { healthApi } from '@/api/healthApi'; function formatBuildLabel(version: string, sha: string): string { const shortSha = sha && sha !== 'unknown' && sha !== 'dev' ? sha.slice(0, 12) : sha; return `${version} (${shortSha})`; } const navIconByKey: Record> = { '/dashboard': LayoutDashboard, '/inbox/reports': Inbox, '/inbox/tickets': Ticket, '/users': Users, '/calendars': Calendar, '/events': CalendarDays, '/reports': AlertTriangle, '/reviews': Star, '/banned-words': Ban, '/tickets': Ticket, '/subscriptions': CreditCard, '/admins': Shield, '/audit': FileSearch, '/monitoring': Activity, }; function NavLinkItem({ item, active, collapsed }: { item: NavItem; active: boolean; collapsed: boolean }) { const { t } = useTranslation(); const Icon = navIconByKey[item.key] ?? LayoutDashboard; const label = t(item.label); return ( {!collapsed && {label}} ); } const ControlCenterLayout: React.FC = () => { useAdminWebSocket(); const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useAuthStore(); const updateProfile = useUpdateProfile(); const [collapsed, setCollapsed] = useState(false); const [paletteOpen, setPaletteOpen] = useState(false); const [density, setDensity] = useState(() => getTableDensity()); const [apiVersionLabel, setApiVersionLabel] = useState(null); const openPalette = useCallback(() => setPaletteOpen(true), []); useCommandPaletteHotkey(openPalette); const adminVersionLabel = formatBuildLabel( import.meta.env.VITE_APP_VERSION || '0.0', import.meta.env.VITE_GIT_SHA || 'dev' ); useEffect(() => { let cancelled = false; healthApi .getAdminHealth() .then((info) => { if (cancelled) return; setApiVersionLabel( formatBuildLabel(info.version || '0.0', info.git_sha || 'unknown') ); }) .catch(() => { if (!cancelled) setApiVersionLabel(null); }); return () => { cancelled = true; }; }, []); const selectedKey = getMenuSelectedKey(location.pathname); const groups = useMemo(() => filterNavGroupsByRole(NAV_GROUPS, user?.role), [user?.role]); useEffect(() => { const handler = (event: CustomEvent) => { const msg = event.detail; if (msg.type === 'report_created') { if (!canReceiveWsNotification('report_created', user?.role)) return; const { report_id, target_type, reason } = msg.data || {}; const inboxPath = report_id ? `/inbox/reports?selected=${report_id}` : '/inbox/reports'; notify.info( [ report_id ? t('ws.reportId', { id: report_id }) : t('ws.newReport'), target_type ? t('ws.onTarget', { type: target_type }) : '', reason ? `(${reason})` : '', ] .filter(Boolean) .join(' '), { action: { label: t('ws.open'), onClick: () => navigate(inboxPath), }, } ); } else if (msg.type === 'ticket_created') { if (!canReceiveWsNotification('ticket_created', user?.role)) return; const { ticket_id } = msg.data || {}; const inboxPath = ticket_id ? `/inbox/tickets?selected=${ticket_id}` : '/inbox/tickets'; notify.info( ticket_id ? t('ws.ticketCreated', { id: ticket_id }) : t('ws.newTicket'), { action: { label: t('ws.open'), onClick: () => navigate(inboxPath), }, } ); } }; window.addEventListener('admin-ws-message', handler as EventListener); return () => window.removeEventListener('admin-ws-message', handler as EventListener); }, [navigate, user?.role, t]); const handleLogout = async () => { await logout(); navigate('/login'); }; const handleDensityToggle = () => { const next = toggleTableDensity(); setDensity(next); notify.info(next === 'compact' ? t('layout.densityCompact') : t('layout.densityNormal')); }; const displayName = (() => { const nick = user?.nickname; const email = user?.email; if (nick && nick !== '-' && nick !== 'undefined') return nick; if (email && email !== '-' && email !== 'undefined') return email; return user?.id ?? t('common.emDash'); })(); const initials = displayName.slice(0, 2).toUpperCase(); return (
{displayName} {user?.role}
navigate('/profile')}> {t('layout.myProfile')} {t('layout.uiLanguage')} { if ((user?.language || 'ru') === 'ru') return; updateProfile.mutate({ language: 'ru' }); }} > {t('common.langRu')} { if (user?.language === 'en') return; updateProfile.mutate({ language: 'en' }); }} > {t('common.langEn')}
{t('layout.adminVersion', { version: adminVersionLabel })}
{apiVersionLabel ? t('layout.apiVersion', { version: apiVersionLabel }) : t('layout.apiVersionUnavailable')}
{t('layout.logout')}
); }; export default ControlCenterLayout;