Files
aerbim-ht-monitor/frontend/components/ui/Sidebar.tsx

474 lines
20 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'
import React, { useState, useEffect } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import Image from 'next/image'
import useUIStore from '../../app/store/uiStore'
import useNavigationStore from '../../app/store/navigationStore'
import { useNavigationService } from '@/services/navigationService'
interface NavigationItem {
id: number
label: string
icon: React.ComponentType<{ className?: string }>
}
interface SidebarProps {
navigationItems?: NavigationItem[]
logoSrc?: string
userInfo?: {
name: string
role: string
avatar?: string
}
activeItem?: number | null
onCustomItemClick?: (itemId: number) => boolean
}
// Иконки-заглушки
const BookOpen = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L3 7v10c0 5.55 3.84 10 9 10s9-4.45 9-10V7l-9-5z" />
</svg>
)
const Bot = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
</svg>
)
const SquareTerminal = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z" />
</svg>
)
const CircleDot = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="3" />
</svg>
)
const BellDot = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C8.13 2 5 5.13 5 9v7l-2 2v1h16v-1l-2-2V9c0-3.87-3.13-7-7-7z" />
</svg>
)
const History = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z" />
</svg>
)
const Settings2 = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" />
</svg>
)
const Monitor = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M20 3H4c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h3l-1 1v1h12v-1l-1-1h3c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 13H4V5h16v11z" />
<circle cx="8" cy="10" r="2" />
<circle cx="16" cy="10" r="2" />
</svg>
)
const Building = ({ className }: { className?: string }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7v10c0 5.55 3.84 10 9 10s9-4.45 9-10V7l-9-5zM12 4.14L18 7.69V17c0 3.31-2.69 6-6 6s-6-2.69-6-6V7.69L12 4.14z" />
<rect x="8" y="10" width="2" height="2" />
<rect x="14" y="10" width="2" height="2" />
<rect x="8" y="14" width="2" height="2" />
<rect x="14" y="14" width="2" height="2" />
</svg>
)
// основные routes
const mainNavigationItems: NavigationItem[] = [
{
id: 1,
icon: BookOpen,
label: 'Дашборд'
},
{
id: 2,
icon: Bot,
label: 'Навигация по зданию'
},
{
id: 8,
icon: History,
label: 'История тревог'
},
{
id: 9,
icon: Settings2,
label: 'Отчеты'
}
]
// суб-меню под "Навигация по зданию"
const navigationSubItems: NavigationItem[] = [
{
id: 3,
icon: Monitor,
label: 'Зоны Мониторинга'
},
{
id: 4,
icon: Building,
label: 'Навигация по этажам'
},
{
id: 5,
icon: BellDot,
label: 'Уведомления'
},
{
id: 6,
icon: CircleDot,
label: 'Сенсоры'
},
{
id: 7,
icon: SquareTerminal,
label: 'Список датчиков'
}
]
const Sidebar: React.FC<SidebarProps> = ({
logoSrc,
userInfo = {
name: 'Александр',
role: 'Администратор'
},
activeItem: propActiveItem,
onCustomItemClick
}) => {
const navigationService = useNavigationService()
const router = useRouter()
const pathname = usePathname()
const [internalActiveItem, setInternalActiveItem] = useState<number | null>(null)
const [isHydrated, setIsHydrated] = useState(false)
const [manuallyToggled, setManuallyToggled] = useState(false)
const activeItem = propActiveItem !== undefined ? propActiveItem : internalActiveItem
const {
isSidebarCollapsed: isCollapsed,
toggleSidebar,
isNavigationSubMenuExpanded: showNavigationSubItems,
setNavigationSubMenuExpanded: setShowNavigationSubItems,
toggleNavigationSubMenu
} = useUIStore()
const {
openMonitoring,
openFloorNavigation,
openNotifications,
closeMonitoring,
closeFloorNavigation,
closeNotifications,
showMonitoring,
showFloorNavigation,
showNotifications
} = useNavigationStore()
useEffect(() => {
setIsHydrated(true)
}, [])
// Чек если суб-меню активны
const isNavigationSubItemActive = activeItem && [3, 4, 5, 6, 7].includes(activeItem)
const shouldShowNavigationAsActive = activeItem === 2 || isNavigationSubItemActive
// Авто-расткрытие меню, если суб-меню стало активным (только если не было ручного переключения)
useEffect(() => {
if (isNavigationSubItemActive && !showNavigationSubItems && !manuallyToggled) {
setShowNavigationSubItems(true)
}
}, [isNavigationSubItemActive, showNavigationSubItems, manuallyToggled, setShowNavigationSubItems])
const handleItemClick = (itemId: number) => {
let handled = false
// Handle submenu items directly with navigation store
switch (itemId) {
case 2: // Navigation - only navigate to page, don't open submenus
if (pathname !== '/navigation') {
router.push('/navigation')
}
handled = true
break
case 3: // Monitoring
if (pathname !== '/navigation') {
// Navigate to navigation page first, then open monitoring
router.push('/navigation')
setTimeout(() => openMonitoring(), 100)
} else if (showMonitoring) {
// Close if already open
closeMonitoring()
} else {
openMonitoring()
}
handled = true
break
case 4: // Floor Navigation
if (pathname !== '/navigation') {
// Navigate to navigation page first, then open floor navigation
router.push('/navigation')
setTimeout(() => openFloorNavigation(), 100)
} else if (showFloorNavigation) {
// Close if already open
closeFloorNavigation()
} else {
openFloorNavigation()
}
handled = true
break
case 5: // Notifications
if (pathname !== '/navigation') {
// Navigate to navigation page first, then open notifications
router.push('/navigation')
setTimeout(() => openNotifications(), 100)
} else if (showNotifications) {
// Close if already open
closeNotifications()
} else {
openNotifications()
}
handled = true
break
default:
// For other items, use navigation service
if (navigationService) {
handled = navigationService.handleSidebarItemClick(itemId, pathname)
}
break
}
if (handled) {
// Update internal active item state
if (propActiveItem === undefined) {
setInternalActiveItem(itemId)
}
// Call custom handler if provided (for additional logic)
if (onCustomItemClick) {
onCustomItemClick(itemId)
}
}
}
return (
<aside
className={`flex flex-col items-start gap-6 relative bg-[#161824] transition-all duration-300 h-screen ${
isCollapsed ? 'w-16' : 'w-64'
}`}
role="navigation"
aria-label="Main navigation"
>
<header className="flex items-center gap-2 pt-2 pb-2 px-4 relative self-stretch w-full flex-[0_0_auto] bg-[#161824]">
{!isCollapsed && (
<div className="relative w-[169.83px] h-[34px]">
{logoSrc && (
<Image
className="absolute w-12 h-[33px] top-0 left-0"
alt="AerBIM Monitor Logo"
src={logoSrc}
width={48}
height={33}
/>
)}
<div className="absolute w-[99px] top-[21px] left-[50px] [font-family:'Open_Sans-Regular',Helvetica] font-normal text-[#389ee8] text-[13px] tracking-[0] leading-[13px] whitespace-nowrap">
AMS HT Viewer
</div>
<div className="absolute top-px left-[50px] [font-family:'Open_Sans-SemiBold',Helvetica] font-semibold text-[#f1f6fa] text-[15px] tracking-[0] leading-[15px] whitespace-nowrap">
AerBIM Monitor
</div>
</div>
)}
</header>
<nav className="flex flex-col items-end gap-2 relative flex-1 self-stretch w-full grow">
<div className="flex flex-col items-end gap-2 px-4 py-2 relative self-stretch w-full flex-[0_0_auto]">
<ul className="flex flex-col items-start gap-3 relative self-stretch w-full flex-[0_0_auto]" role="list">
{mainNavigationItems.map((item) => {
const IconComponent = item.icon
const isActive = item.id === 2 ? shouldShowNavigationAsActive : activeItem === item.id
return (
<li key={item.id} className="flex-col flex items-center relative self-stretch w-full" role="listitem">
<button
className={`gap-2 pt-2 pr-2 pb-2 pl-2 rounded-md flex h-9 items-center relative self-stretch w-full transition-colors duration-200 hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset ${
isActive ? 'bg-gray-700' : ''
}`}
onClick={() => handleItemClick(item.id)}
aria-current={isActive ? 'page' : undefined}
type="button"
>
<IconComponent
className="!relative !w-5 !h-5 text-white"
aria-hidden="true"
/>
{!isCollapsed && (
<span className="flex-1 [font-family:'Inter-Regular',Helvetica] font-normal text-white text-sm leading-[14px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical] text-left">
{item.label}
</span>
)}
{item.id === 2 && !isCollapsed && (
<div
className="p-1 hover:bg-gray-600 rounded transition-colors duration-200 cursor-pointer"
onClick={(e) => {
e.stopPropagation()
setManuallyToggled(true)
// Close all active submenus when collapsing navigation menu
if (showNavigationSubItems || isNavigationSubItemActive) {
closeMonitoring()
closeFloorNavigation()
closeNotifications()
}
toggleNavigationSubMenu()
}}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
e.stopPropagation()
setManuallyToggled(true)
// Close all active submenus when collapsing navigation menu
if (showNavigationSubItems || isNavigationSubItemActive) {
closeMonitoring()
closeFloorNavigation()
closeNotifications()
}
toggleNavigationSubMenu()
}
}}
aria-label={isHydrated ? (showNavigationSubItems || isNavigationSubItemActive ? 'Collapse navigation menu' : 'Expand navigation menu') : 'Toggle navigation menu'}
>
<svg
className={`!relative !w-4 !h-4 text-white transition-transform duration-200 ${
isHydrated && (showNavigationSubItems || isNavigationSubItemActive) ? 'rotate-90' : ''
}`}
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z" />
</svg>
</div>
)}
</button>
{/* Суб-меню */}
{item.id === 2 && !isCollapsed && (showNavigationSubItems || isNavigationSubItemActive) && (
<ul className="flex flex-col items-start gap-1 mt-1 ml-6 relative w-full" role="list">
{navigationSubItems.map((subItem) => {
const SubIconComponent = subItem.icon
const isSubActive = activeItem === subItem.id
return (
<li key={subItem.id} className="flex-col flex h-8 items-center relative self-stretch w-full" role="listitem">
<button
className={`gap-2 pt-1.5 pr-2 pb-1.5 pl-2 rounded-md flex h-8 items-center relative self-stretch w-full transition-colors duration-200 hover:bg-gray-600 focus:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset ${
isSubActive ? 'bg-gray-600' : ''
}`}
onClick={() => handleItemClick(subItem.id)}
aria-current={isSubActive ? 'page' : undefined}
type="button"
>
<SubIconComponent
className="!relative !w-4 !h-4 text-gray-300"
aria-hidden="true"
/>
<span className="flex-1 [font-family:'Inter-Regular',Helvetica] font-normal text-gray-300 text-sm leading-[14px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical] text-left">
{subItem.label}
</span>
</button>
</li>
)
})}
</ul>
)}
</li>
)
})}
</ul>
<button
className="!relative !w-6 !h-6 p-1 rounded hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200"
onClick={() => {
// If navigation submenu is open, first collapse it (which closes all submenus)
if (showNavigationSubItems) {
setShowNavigationSubItems(false)
setManuallyToggled(true)
// Close all active submenus when collapsing navigation menu
closeMonitoring()
closeFloorNavigation()
closeNotifications()
}
// Always collapse the sidebar
toggleSidebar()
}}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
type="button"
>
<svg className={`!relative !w-4 !h-4 text-white transition-transform duration-200 ${isCollapsed ? 'rotate-180' : ''}`} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
</svg>
</button>
</div>
</nav>
{!isCollapsed && (
<footer className="flex w-64 items-center gap-2 pt-2 pr-2 pb-2 pl-2 mt-auto bg-[#161824]">
<div className="inline-flex flex-[0_0_auto] items-center gap-2 relative rounded-md">
<div className="inline-flex items-center relative flex-[0_0_auto]">
<div
className="relative w-8 h-8 rounded-lg bg-white"
role="img"
aria-label="User avatar"
>
{userInfo.avatar && (
<Image
src={userInfo.avatar}
alt="User avatar"
className="w-full h-full rounded-lg object-cover"
fill
sizes="32px"
/>
)}
</div>
</div>
</div>
<div className="flex p-2 flex-1 grow items-center gap-2 relative rounded-md">
<div className="flex flex-col items-start justify-center gap-0.5 relative flex-1 grow">
<div className="self-stretch mt-[-1.00px] [font-family:'Inter-SemiBold',Helvetica] font-semibold text-white text-sm leading-[14px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical]">
{userInfo.name}
</div>
<div className="self-stretch [font-family:'Inter-Regular',Helvetica] font-normal text-[#71717a] text-[10px] leading-[10px] relative tracking-[0] overflow-hidden text-ellipsis [display:-webkit-box] [-webkit-line-clamp:1] [-webkit-box-orient:vertical]">
{userInfo.role}
</div>
</div>
</div>
<button
className="relative w-4 h-4 aspect-[1] p-1 rounded hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-200"
aria-label="User menu"
type="button"
>
<svg className="w-3.5 h-3.5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>
</button>
</footer>
)}
</aside>
)
}
export default Sidebar