diff --git a/src/features/control-center/components/ControlCenterShell.tsx b/src/features/control-center/components/ControlCenterShell.tsx index 3429e76..52e5e3a 100644 --- a/src/features/control-center/components/ControlCenterShell.tsx +++ b/src/features/control-center/components/ControlCenterShell.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react'; -import { NavLink, Outlet } from 'react-router-dom'; +import React, { useEffect, useState, useMemo } from 'react'; +import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { LayoutDashboard, Building2, @@ -14,24 +14,48 @@ import { CheckCircle2, Radio, ExternalLink, + Menu, + X, + ShieldAlert, + Sun, + Moon, + Sparkles, + ShieldCheck, + ChevronRight, + SlidersHorizontal, + ChevronLeft, } from 'lucide-react'; -import { useKccStore } from '../store/kccStore'; +import { useKccStore, useOperatorRole } from '../store/kccStore'; import { OperatorSessionSwitcher } from './OperatorSessionSwitcher'; import { InspectorDrawer } from './InspectorDrawer'; import { ConfirmOperatorActionModal } from './ConfirmOperatorAction'; import { ServerMonitorOutageBanner } from './ServerMonitorOutageBanner'; +import { useThemeStore, ThemeMode } from '../../../stores/theme'; +import { RU_ROLES, RU_THEMES, RU_UI } from '../locales/ru'; export const ControlCenterShell: React.FC = () => { + const location = useLocation(); + const navigate = useNavigate(); + const isInitialized = useKccStore((state) => state.isInitialized); const initialize = useKccStore((state) => state.initialize); const resetToDefaults = useKccStore((state) => state.resetToDefaults); const serverMonitorOutage = useKccStore((state) => state.serverMonitorOutage); const toggleServerMonitorOutage = useKccStore((state) => state.toggleServerMonitorOutage); + const incidents = useKccStore((state) => state.incidents); + + const { role, operator } = useOperatorRole(); + const themeMode = useThemeStore((state) => state.themeMode); + const setTheme = useThemeStore((state) => state.setTheme); const [utcTime, setUtcTime] = useState(''); const [isResetting, setIsResetting] = useState(false); const [showSavedFeedback, setShowSavedFeedback] = useState(false); + // Mobile / Tablet Drawer states + const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false); + const [isTabletSidebarCollapsed, setIsTabletSidebarCollapsed] = useState(false); + useEffect(() => { if (!isInitialized) { initialize(); @@ -48,8 +72,13 @@ export const ControlCenterShell: React.FC = () => { return () => clearInterval(interval); }, []); + // Close mobile drawer on route change + useEffect(() => { + setIsMobileDrawerOpen(false); + }, [location.pathname]); + const handleReset = async () => { - if (confirm('Сбросить состояние демонстрационной базы данных к исходному состоянию?')) { + if (confirm('Сбросить демонстрационные данные платформы к исходному состоянию?')) { setIsResetting(true); await resetToDefaults(); setIsResetting(false); @@ -58,107 +87,260 @@ export const ControlCenterShell: React.FC = () => { } }; - const navItems = [ + // Critical events calculation + const criticalCount = useMemo(() => { + let count = 0; + if (serverMonitorOutage) count += 1; + count += incidents.filter( + (i) => i.status !== 'resolved' && (i.severity === 'P0_CRITICAL' || i.severity === 'P1_MAJOR') + ).length; + return count; + }, [serverMonitorOutage, incidents]); + + // Navigation Items + const primaryMobileNav = [ { to: '/control', label: 'Обзор', icon: LayoutDashboard, end: true }, { to: '/control/organizations', label: 'Организации', icon: Building2 }, - { to: '/control/billing', label: 'Подписки и биллинг', icon: CreditCard }, - { to: '/control/modules', label: 'Каталог модулей', icon: Package }, - { to: '/control/installations', label: 'Установки и клиенты', icon: Smartphone }, - { to: '/control/infrastructure', label: 'Инфраструктура', icon: Server }, - { to: '/control/announcements', label: 'Сообщения', icon: BellRing }, - { to: '/control/incidents', label: 'Инциденты', icon: AlertTriangle }, - { to: '/control/audit', label: 'Неизменяемый аудит', icon: FileCheck2 }, + { to: '/control/incidents', label: 'Инциденты', icon: AlertTriangle, badge: criticalCount > 0 ? criticalCount : undefined }, + { to: '/control/infrastructure', label: 'Серверы', icon: Server }, ]; + const allNavItems = [ + { to: '/control', label: 'Обзор', icon: LayoutDashboard, end: true }, + { to: '/control/organizations', label: 'Организации', icon: Building2 }, + { to: '/control/billing', label: 'Подписки и платежи', icon: CreditCard }, + { to: '/control/modules', label: 'Каталог модулей', icon: Package }, + { to: '/control/installations', label: 'Установки', icon: Smartphone }, + { to: '/control/infrastructure', label: 'Серверы', icon: Server }, + { to: '/control/announcements', label: 'Оповещения', icon: BellRing }, + { to: '/control/incidents', label: 'Инциденты', icon: AlertTriangle, badge: criticalCount > 0 ? criticalCount : undefined }, + { to: '/control/audit', label: 'Журнал аудита', icon: FileCheck2 }, + ]; + + const moreNavItems = [ + { to: '/control/billing', label: 'Подписки и платежи', icon: CreditCard }, + { to: '/control/modules', label: 'Каталог модулей', icon: Package }, + { to: '/control/installations', label: 'Установки', icon: Smartphone }, + { to: '/control/announcements', label: 'Оповещения', icon: BellRing }, + { to: '/control/audit', label: 'Журнал аудита', icon: FileCheck2 }, + ]; + + // Current section title + const currentTitle = useMemo(() => { + const current = allNavItems.find((item) => + item.end ? location.pathname === item.to : location.pathname.startsWith(item.to) + ); + return current?.label || 'Control Center'; + }, [location.pathname, allNavItems]); + + const currentRoleLabel = RU_ROLES[role]?.shortLabel || role; + return ( -