Добавлена иконка на боковой тултип для отображения/скрытия подсветки датчиков на модели

This commit is contained in:
2026-02-03 13:13:16 +03:00
parent 81b361b282
commit eccc564cc7
9 changed files with 2135 additions and 9 deletions

View File

@@ -103,7 +103,9 @@ const NavigationPage: React.FC = () => {
setSelectedNotification,
setShowNotificationDetectorInfo,
setSelectedAlert,
setShowAlertMenu
setShowAlertMenu,
showSensorHighlights,
toggleSensorHighlights
} = useNavigationStore()
const [detectorsData, setDetectorsData] = useState<{ detectors: Record<string, DetectorType> }>({ detectors: {} })
@@ -574,7 +576,7 @@ const NavigationPage: React.FC = () => {
onError={handleModelError}
activeMenu={showSensors ? 'sensors' : showFloorNavigation ? 'floor' : showListOfDetectors ? 'detectors' : null}
focusSensorId={focusedSensorId}
highlightAllSensors={highlightAllSensors}
highlightAllSensors={showSensorHighlights && highlightAllSensors}
sensorStatusMap={sensorStatusMap}
isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors}
onSensorPick={handleSensorSelection}

View File

@@ -0,0 +1,613 @@
'use client'
import React, { useEffect, useCallback, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import Image from 'next/image'
import Sidebar from '../../../components/ui/Sidebar'
import AnimatedBackground from '../../../components/ui/AnimatedBackground'
import useNavigationStore from '../../store/navigationStore'
import Monitoring from '../../../components/navigation/Monitoring'
import FloorNavigation from '../../../components/navigation/FloorNavigation'
import DetectorMenu from '../../../components/navigation/DetectorMenu'
import ListOfDetectors from '../../../components/navigation/ListOfDetectors'
import Sensors from '../../../components/navigation/Sensors'
import AlertMenu from '../../../components/navigation/AlertMenu'
import Notifications from '../../../components/notifications/Notifications'
import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo'
import dynamic from 'next/dynamic'
import type { ModelViewerProps } from '../../../components/model/ModelViewer'
import * as statusColors from '../../../lib/statusColors'
const ModelViewer = dynamic<ModelViewerProps>(() => import('../../../components/model/ModelViewer'), {
ssr: false,
loading: () => (
<div className="w-full h-full flex items-center justify-center bg-[#0e111a]">
<div className="text-gray-300 animate-pulse">Загрузка 3D-модуля…</div>
</div>
),
})
interface DetectorType {
detector_id: number
name: string
serial_number: string
object: string
status: string
checked: boolean
type: string
detector_type: string
location: string
floor: number
notifications: Array<{
id: number
type: string
message: string
timestamp: string
acknowledged: boolean
priority: string
}>
}
interface NotificationType {
id: number
detector_id: number
detector_name: string
type: string
status: string
message: string
timestamp: string
location: string
object: string
acknowledged: boolean
priority: string
}
interface AlertType {
id: number
detector_id: number
detector_name: string
type: string
status: string
message: string
timestamp: string
location: string
object: string
acknowledged: boolean
priority: string
}
const NavigationPage: React.FC = () => {
const router = useRouter()
const searchParams = useSearchParams()
const {
currentObject,
setCurrentObject,
showMonitoring,
showFloorNavigation,
showNotifications,
showListOfDetectors,
showSensors,
selectedDetector,
showDetectorMenu,
selectedNotification,
showNotificationDetectorInfo,
selectedAlert,
showAlertMenu,
closeMonitoring,
closeFloorNavigation,
closeNotifications,
closeListOfDetectors,
closeSensors,
setSelectedDetector,
setShowDetectorMenu,
setSelectedNotification,
setShowNotificationDetectorInfo,
setSelectedAlert,
setShowAlertMenu
} = useNavigationStore()
const [detectorsData, setDetectorsData] = useState<{ detectors: Record<string, DetectorType> }>({ detectors: {} })
const [detectorsError, setDetectorsError] = useState<string | null>(null)
const [modelError, setModelError] = useState<string | null>(null)
const [isModelReady, setIsModelReady] = useState(false)
const [focusedSensorId, setFocusedSensorId] = useState<string | null>(null)
const [highlightAllSensors, setHighlightAllSensors] = useState(false)
const sensorStatusMap = React.useMemo(() => {
const map: Record<string, string> = {}
Object.values(detectorsData.detectors).forEach(d => {
if (d.serial_number && d.status) {
map[String(d.serial_number).trim()] = d.status
}
})
console.log('[NavigationPage] sensorStatusMap created with', Object.keys(map).length, 'sensors')
console.log('[NavigationPage] Sample sensor IDs in map:', Object.keys(map).slice(0, 5))
return map
}, [detectorsData])
useEffect(() => {
if (selectedDetector === null && selectedAlert === null) {
setFocusedSensorId(null);
}
}, [selectedDetector, selectedAlert]);
// Управление выделением всех сенсоров при открытии/закрытии меню Sensors
// ИСПРАВЛЕНО: Подсветка датчиков остается включенной всегда, независимо от состояния панели Sensors
useEffect(() => {
console.log('[NavigationPage] showSensors changed:', showSensors, 'modelReady:', isModelReady)
if (isModelReady) {
// Всегда включаем подсветку всех сенсоров когда модель готова
console.log('[NavigationPage] Setting highlightAllSensors to TRUE (always enabled)')
setHighlightAllSensors(true)
// Сбрасываем фокус только если панель Sensors закрыта
if (!showSensors) {
setFocusedSensorId(null)
}
}
}, [showSensors, isModelReady])
// Дополнительный эффект для задержки выделения сенсоров при открытии меню
// ИСПРАВЛЕНО: Задержка применяется только при открытии панели Sensors
useEffect(() => {
if (showSensors && isModelReady) {
const timer = setTimeout(() => {
console.log('[NavigationPage] Delayed highlightAllSensors to TRUE')
setHighlightAllSensors(true)
}, 500) // Задержка 500мс для полной инициализации модели
return () => clearTimeout(timer)
}
}, [showSensors, isModelReady])
const urlObjectId = searchParams.get('objectId')
const urlObjectTitle = searchParams.get('objectTitle')
const urlModelPath = searchParams.get('modelPath')
const objectId = currentObject.id || urlObjectId
const objectTitle = currentObject.title || urlObjectTitle
const [selectedModelPath, setSelectedModelPath] = useState<string>(urlModelPath || '')
const handleModelLoaded = useCallback(() => {
setIsModelReady(true)
setModelError(null)
}, [])
const handleModelError = useCallback((error: string) => {
console.error('[NavigationPage] Model loading error:', error)
setModelError(error)
setIsModelReady(false)
}, [])
useEffect(() => {
if (selectedModelPath) {
setIsModelReady(false);
setModelError(null);
// Сохраняем выбранную модель в URL для восстановления при возврате
const params = new URLSearchParams(searchParams.toString());
params.set('modelPath', selectedModelPath);
window.history.replaceState(null, '', `?${params.toString()}`);
}
}, [selectedModelPath, searchParams]);
useEffect(() => {
if (urlObjectId && (!currentObject.id || currentObject.id !== urlObjectId)) {
setCurrentObject(urlObjectId, urlObjectTitle ?? currentObject.title ?? undefined)
}
}, [urlObjectId, urlObjectTitle, currentObject.id, currentObject.title, setCurrentObject])
// Восстановление выбранной модели из URL при загрузке страницы
useEffect(() => {
if (urlModelPath && !selectedModelPath) {
setSelectedModelPath(urlModelPath);
}
}, [urlModelPath, selectedModelPath])
useEffect(() => {
const loadDetectors = async () => {
try {
setDetectorsError(null)
const res = await fetch('/api/get-detectors', { cache: 'no-store' })
const text = await res.text()
let payload: any
try { payload = JSON.parse(text) } catch { payload = text }
console.log('[NavigationPage] GET /api/get-detectors', { status: res.status, payload })
if (!res.ok) throw new Error(typeof payload === 'string' ? payload : (payload?.error || 'Не удалось получить детекторов'))
const data = payload?.data ?? payload
const detectors = (data?.detectors ?? {}) as Record<string, DetectorType>
console.log('[NavigationPage] Received detectors count:', Object.keys(detectors).length)
console.log('[NavigationPage] Sample detector keys:', Object.keys(detectors).slice(0, 5))
setDetectorsData({ detectors })
} catch (e: any) {
console.error('Ошибка загрузки детекторов:', e)
setDetectorsError(e?.message || 'Ошибка при загрузке детекторов')
}
}
loadDetectors()
}, [])
const handleBackClick = () => {
router.push('/dashboard')
}
const handleDetectorMenuClick = (detector: DetectorType) => {
// Для тестов. Выбор детектора.
console.log('[NavigationPage] Selected detector click:', {
detector_id: detector.detector_id,
name: detector.name,
serial_number: detector.serial_number,
})
// Проверяем, что детектор имеет необходимые данные
if (!detector || !detector.detector_id || !detector.serial_number) {
console.warn('[NavigationPage] Invalid detector data, skipping menu display:', detector)
return
}
if (selectedDetector?.serial_number === detector.serial_number && showDetectorMenu) {
closeDetectorMenu()
} else {
setSelectedDetector(detector)
setShowDetectorMenu(true)
setFocusedSensorId(detector.serial_number)
setShowAlertMenu(false)
setSelectedAlert(null)
// При открытии меню детектора - сбрасываем множественное выделение
setHighlightAllSensors(false)
}
}
const closeDetectorMenu = () => {
setShowDetectorMenu(false)
setSelectedDetector(null)
setFocusedSensorId(null)
setSelectedAlert(null)
// При закрытии меню детектора - выделяем все сенсоры снова
setHighlightAllSensors(true)
}
const handleNotificationClick = (notification: NotificationType) => {
if (selectedNotification?.id === notification.id && showNotificationDetectorInfo) {
setShowNotificationDetectorInfo(false)
setSelectedNotification(null)
} else {
setSelectedNotification(notification)
setShowNotificationDetectorInfo(true)
}
}
const closeNotificationDetectorInfo = () => {
setShowNotificationDetectorInfo(false)
setSelectedNotification(null)
}
const closeAlertMenu = () => {
setShowAlertMenu(false)
setSelectedAlert(null)
setFocusedSensorId(null)
setSelectedDetector(null)
// При закрытии меню алерта - выделяем все сенсоры снова
setHighlightAllSensors(true)
}
const handleAlertClick = (alert: AlertType) => {
console.log('[NavigationPage] Alert clicked, focusing on detector in 3D scene:', alert)
const detector = Object.values(detectorsData.detectors).find(
d => d.detector_id === alert.detector_id
)
if (detector) {
if (selectedAlert?.id === alert.id && showAlertMenu) {
closeAlertMenu()
} else {
setSelectedAlert(alert)
setShowAlertMenu(true)
setFocusedSensorId(detector.serial_number)
setShowDetectorMenu(false)
setSelectedDetector(null)
// При открытии меню алерта - сбрасываем множественное выделение
setHighlightAllSensors(false)
console.log('[NavigationPage] Showing AlertMenu for alert:', alert.detector_name)
}
} else {
console.warn('[NavigationPage] Could not find detector for alert:', alert.detector_id)
}
}
const handleSensorSelection = (serialNumber: string | null) => {
if (serialNumber === null) {
setFocusedSensorId(null);
closeDetectorMenu();
closeAlertMenu();
// If we're in Sensors menu and no sensor is selected, highlight all sensors
if (showSensors) {
setHighlightAllSensors(true);
}
return;
}
if (focusedSensorId === serialNumber) {
setFocusedSensorId(null);
closeDetectorMenu();
closeAlertMenu();
// If we're in Sensors menu and deselected the current sensor, highlight all sensors
if (showSensors) {
setHighlightAllSensors(true);
}
return;
}
// При выборе конкретного сенсора - сбрасываем множественное выделение
setHighlightAllSensors(false)
const detector = Object.values(detectorsData?.detectors || {}).find(
(d) => d.serial_number === serialNumber
);
if (detector) {
if (showFloorNavigation || showListOfDetectors) {
handleDetectorMenuClick(detector);
} else if (detector.notifications && detector.notifications.length > 0) {
const sortedNotifications = [...detector.notifications].sort((a, b) => {
const priorityOrder: { [key: string]: number } = { critical: 0, warning: 1, info: 2 };
return priorityOrder[a.priority.toLowerCase()] - priorityOrder[b.priority.toLowerCase()];
});
const notification = sortedNotifications[0];
const alert: AlertType = {
...notification,
detector_id: detector.detector_id,
detector_name: detector.name,
location: detector.location,
object: detector.object,
status: detector.status,
type: notification.type || 'info',
};
handleAlertClick(alert);
} else {
handleDetectorMenuClick(detector);
}
} else {
setFocusedSensorId(null);
closeDetectorMenu();
closeAlertMenu();
// If we're in Sensors menu and no valid detector found, highlight all sensors
if (showSensors) {
setHighlightAllSensors(true);
}
}
};
const getStatusText = (status: string) => {
const s = (status || '').toLowerCase()
switch (s) {
case statusColors.STATUS_COLOR_CRITICAL:
case 'critical':
return 'Критический'
case statusColors.STATUS_COLOR_WARNING:
case 'warning':
return 'Предупреждение'
case statusColors.STATUS_COLOR_NORMAL:
case 'normal':
return 'Норма'
default:
return 'Неизвестно'
}
}
return (
<div className="relative flex h-screen bg-[#0e111a] overflow-hidden">
<AnimatedBackground />
<div className="relative z-20">
<Sidebar
activeItem={2}
/>
</div>
<div className="relative z-10 flex-1 flex flex-col">
{showMonitoring && (
<div className="absolute left-0 top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-20 w-[500px]">
<div className="h-full overflow-auto p-4">
<Monitoring
onClose={closeMonitoring}
onSelectModel={(path) => {
console.log('[NavigationPage] Model selected:', path);
setSelectedModelPath(path)
setModelError(null)
setIsModelReady(false)
}}
/>
</div>
</div>
)}
{showFloorNavigation && (
<div className="absolute left-0 top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-20 w-[500px]">
<div className="h-full overflow-auto p-4">
<FloorNavigation
objectId={objectId || undefined}
detectorsData={detectorsData}
onDetectorMenuClick={handleDetectorMenuClick}
onClose={closeFloorNavigation}
is3DReady={isModelReady && !modelError}
/>
</div>
</div>
)}
{showNotifications && (
<div className="absolute left-0 top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-20 w-[500px]">
<div className="h-full overflow-auto p-4">
<Notifications
objectId={objectId || undefined}
detectorsData={detectorsData}
onNotificationClick={handleNotificationClick}
onClose={closeNotifications}
/>
{detectorsError && (
<div className="mt-2 text-sm text-red-400">{detectorsError}</div>
)}
</div>
</div>
)}
{showListOfDetectors && (
<div className="absolute left-0 top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-20 w-[500px]">
<div className="h-full overflow-auto p-4">
<ListOfDetectors
objectId={objectId || undefined}
detectorsData={detectorsData}
onDetectorMenuClick={handleDetectorMenuClick}
onClose={closeListOfDetectors}
is3DReady={selectedModelPath ? !modelError : false}
/>
{detectorsError && (
<div className="mt-2 text-sm text-red-400">{detectorsError}</div>
)}
</div>
</div>
)}
{showSensors && (
<div className="absolute left-0 top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-20 w-[500px]">
<div className="h-full overflow-auto p-4">
<Sensors
objectId={objectId || undefined}
detectorsData={detectorsData}
onAlertClick={handleAlertClick}
onClose={closeSensors}
is3DReady={selectedModelPath ? !modelError : false}
/>
{detectorsError && (
<div className="mt-2 text-sm text-red-400">{detectorsError}</div>
)}
</div>
</div>
)}
{showNotifications && showNotificationDetectorInfo && selectedNotification && (() => {
const detectorData = Object.values(detectorsData.detectors).find(
detector => detector.detector_id === selectedNotification.detector_id
);
return detectorData ? (
<div className="absolute left-[500px] top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-30 w-[454px]">
<div className="h-full overflow-auto p-4">
<NotificationDetectorInfo
detectorData={detectorData}
onClose={closeNotificationDetectorInfo}
/>
</div>
</div>
) : null;
})()}
{showFloorNavigation && showDetectorMenu && selectedDetector && (
null
)}
<header className="bg-[#161824] border-b border-gray-700 px-6 h-[73px] flex items-center">
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-4">
<button
onClick={handleBackClick}
className="text-gray-400 hover:text-white transition-colors"
aria-label="Назад к дашборду"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<nav className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Дашборд</span>
<span className="text-gray-600">{'/'}</span>
<span className="text-white">{objectTitle || 'Объект'}</span>
<span className="text-gray-600">{'/'}</span>
<span className="text-white">Навигация</span>
</nav>
</div>
</div>
</header>
<div className="flex-1 overflow-hidden">
<div className="h-full">
{modelError ? (
<>
{console.log('[NavigationPage] Rendering error message, modelError:', modelError)}
<div className="h-full flex items-center justify-center bg-[#0e111a]">
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
<div className="text-red-400 text-lg font-semibold mb-4">
Ошибка загрузки 3D модели
</div>
<div className="text-gray-300 mb-4">
{modelError}
</div>
<div className="text-sm text-gray-400">
Используйте навигацию по этажам для просмотра детекторов
</div>
</div>
</div>
</>
) : !selectedModelPath ? (
<div className="h-full flex items-center justify-center bg-[#0e111a]">
<div className="text-center p-8 flex flex-col items-center">
<Image
src="/icons/logo.png"
alt="AerBIM HT Monitor"
width={300}
height={41}
className="mb-6"
/>
<div className="text-gray-300 text-lg">
Выберите модель для отображения
</div>
</div>
</div>
) : (
<ModelViewer
key={selectedModelPath || 'no-model'}
modelPath={selectedModelPath}
onSelectModel={(path) => {
console.log('[NavigationPage] Model selected:', path);
setSelectedModelPath(path)
setModelError(null)
setIsModelReady(false)
}}
onModelLoaded={handleModelLoaded}
onError={handleModelError}
activeMenu={showSensors ? 'sensors' : showFloorNavigation ? 'floor' : showListOfDetectors ? 'detectors' : null}
focusSensorId={focusedSensorId}
highlightAllSensors={highlightAllSensors}
sensorStatusMap={sensorStatusMap}
isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors}
onSensorPick={handleSensorSelection}
renderOverlay={({ anchor }) => (
<>
{selectedAlert && showAlertMenu && anchor ? (
<AlertMenu
alert={selectedAlert}
isOpen={true}
onClose={closeAlertMenu}
getStatusText={getStatusText}
compact={true}
anchor={anchor}
/>
) : selectedDetector && showDetectorMenu && anchor ? (
<DetectorMenu
detector={selectedDetector}
isOpen={true}
onClose={closeDetectorMenu}
getStatusText={getStatusText}
compact={true}
anchor={anchor}
/>
) : null}
</>
)}
/>
)}
</div>
</div>
</div>
</div>
)
}
export default NavigationPage

View File

@@ -68,6 +68,7 @@ export interface NavigationStore {
showNotifications: boolean
showListOfDetectors: boolean
showSensors: boolean
showSensorHighlights: boolean
selectedDetector: DetectorType | null
showDetectorMenu: boolean
@@ -100,6 +101,8 @@ export interface NavigationStore {
closeListOfDetectors: () => void
openSensors: () => void
closeSensors: () => void
toggleSensorHighlights: () => void
setSensorHighlights: (show: boolean) => void
closeAllMenus: () => void
clearSelections: () => void
@@ -137,6 +140,7 @@ const useNavigationStore = create<NavigationStore>()(
showNotifications: false,
showListOfDetectors: false,
showSensors: false,
showSensorHighlights: true,
selectedDetector: null,
showDetectorMenu: false,
@@ -316,6 +320,9 @@ const useNavigationStore = create<NavigationStore>()(
currentSubmenu: null
}),
toggleSensorHighlights: () => set((state) => ({ showSensorHighlights: !state.showSensorHighlights })),
setSensorHighlights: (show: boolean) => set({ showSensorHighlights: show }),
closeAllMenus: () => {
set({
showMonitoring: false,

View File

@@ -0,0 +1,373 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { Zone } from '@/app/types'
export interface DetectorType {
detector_id: number
name: string
serial_number: string
object: string
status: string
type: string
detector_type: string
location: string
floor: number
checked: boolean
notifications: Array<{
id: number
type: string
message: string
timestamp: string
acknowledged: boolean
priority: string
}>
}
interface NotificationType {
id: number
detector_id: number
detector_name: string
type: string
status: string
message: string
timestamp: string
location: string
object: string
acknowledged: boolean
priority: string
}
interface AlertType {
id: number
detector_id: number
detector_name: string
type: string
status: string
message: string
timestamp: string
location: string
object: string
acknowledged: boolean
priority: string
}
export interface NavigationStore {
currentObject: { id: string | undefined; title: string | undefined }
navigationHistory: string[]
currentSubmenu: string | null
currentModelPath: string | null
// Состояния Зон
currentZones: Zone[]
zonesCache: Record<string, Zone[]>
zonesLoading: boolean
zonesError: string | null
showMonitoring: boolean
showFloorNavigation: boolean
showNotifications: boolean
showListOfDetectors: boolean
showSensors: boolean
selectedDetector: DetectorType | null
showDetectorMenu: boolean
selectedNotification: NotificationType | null
showNotificationDetectorInfo: boolean
selectedAlert: AlertType | null
showAlertMenu: boolean
setCurrentObject: (id: string | undefined, title: string | undefined) => void
clearCurrentObject: () => void
addToHistory: (path: string) => void
goBack: () => string | null
setCurrentModelPath: (path: string) => void
setCurrentSubmenu: (submenu: string | null) => void
clearSubmenu: () => void
// Действия с зонами
loadZones: (objectId: string) => Promise<void>
setZones: (zones: Zone[]) => void
clearZones: () => void
openMonitoring: () => void
closeMonitoring: () => void
openFloorNavigation: () => void
closeFloorNavigation: () => void
openNotifications: () => void
closeNotifications: () => void
openListOfDetectors: () => void
closeListOfDetectors: () => void
openSensors: () => void
closeSensors: () => void
closeAllMenus: () => void
clearSelections: () => void
setSelectedDetector: (detector: DetectorType | null) => void
setShowDetectorMenu: (show: boolean) => void
setSelectedNotification: (notification: NotificationType | null) => void
setShowNotificationDetectorInfo: (show: boolean) => void
setSelectedAlert: (alert: AlertType | null) => void
setShowAlertMenu: (show: boolean) => void
isOnNavigationPage: () => boolean
getCurrentRoute: () => string | null
getActiveSidebarItem: () => number
}
const useNavigationStore = create<NavigationStore>()(
persist(
(set, get) => ({
currentObject: {
id: undefined,
title: undefined,
},
navigationHistory: [],
currentSubmenu: null,
currentModelPath: null,
currentZones: [],
zonesCache: {},
zonesLoading: false,
zonesError: null,
showMonitoring: false,
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
showSensors: false,
selectedDetector: null,
showDetectorMenu: false,
selectedNotification: null,
showNotificationDetectorInfo: false,
selectedAlert: null,
showAlertMenu: false,
setCurrentObject: (id: string | undefined, title: string | undefined) =>
set({ currentObject: { id, title } }),
clearCurrentObject: () =>
set({ currentObject: { id: undefined, title: undefined } }),
setCurrentModelPath: (path: string) => set({ currentModelPath: path }),
addToHistory: (path: string) => {
const { navigationHistory } = get()
const newHistory = [...navigationHistory, path]
if (newHistory.length > 10) {
newHistory.shift()
}
set({ navigationHistory: newHistory })
},
goBack: () => {
const { navigationHistory } = get()
if (navigationHistory.length > 1) {
const newHistory = [...navigationHistory]
newHistory.pop()
const previousPage = newHistory.pop()
set({ navigationHistory: newHistory })
return previousPage || null
}
return null
},
setCurrentSubmenu: (submenu: string | null) =>
set({ currentSubmenu: submenu }),
clearSubmenu: () =>
set({ currentSubmenu: null }),
loadZones: async (objectId: string) => {
const cache = get().zonesCache
const cached = cache[objectId]
const hasCached = Array.isArray(cached) && cached.length > 0
if (hasCached) {
// Показываем кэшированные зоны сразу, но обновляем в фоне
set({ currentZones: cached, zonesLoading: true, zonesError: null })
} else {
set({ zonesLoading: true, zonesError: null })
}
try {
const res = await fetch(`/api/get-zones?objectId=${encodeURIComponent(objectId)}`, { cache: 'no-store' })
const text = await res.text()
let payload: string | Record<string, unknown>
try { payload = JSON.parse(text) } catch { payload = text }
if (!res.ok) throw new Error(typeof payload === 'string' ? payload : (payload?.error as string || 'Не удалось получить зоны'))
const zones: Zone[] = typeof payload === 'string' ? [] :
Array.isArray(payload?.data) ? payload.data as Zone[] :
(payload?.data && typeof payload.data === 'object' && 'zones' in payload.data ? (payload.data as { zones?: Zone[] }).zones :
payload?.zones ? payload.zones as Zone[] : []) || []
const normalized = zones.map((z) => ({
...z,
image_path: z.image_path ?? null,
}))
set((state) => ({
currentZones: normalized,
zonesCache: { ...state.zonesCache, [objectId]: normalized },
zonesLoading: false,
zonesError: null,
}))
} catch (e: unknown) {
set({ zonesLoading: false, zonesError: (e as Error)?.message || 'Ошибка при загрузке зон' })
}
},
setZones: (zones: Zone[]) => set({ currentZones: zones }),
clearZones: () => set({ currentZones: [] }),
openMonitoring: () => {
set({
showMonitoring: true,
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
currentSubmenu: 'monitoring',
showDetectorMenu: false,
selectedDetector: null,
showNotificationDetectorInfo: false,
selectedNotification: null,
zonesError: null // Очищаем ошибку зон при открытии мониторинга
})
const objId = get().currentObject.id
if (objId) {
// Вызываем загрузку зон сразу, но обновляем в фоне
get().loadZones(objId)
}
},
closeMonitoring: () => set({
showMonitoring: false,
currentSubmenu: null
}),
openFloorNavigation: () => set({
showFloorNavigation: true,
showMonitoring: false,
showNotifications: false,
showListOfDetectors: false,
currentSubmenu: 'floors',
showNotificationDetectorInfo: false,
selectedNotification: null
}),
closeFloorNavigation: () => set({
showFloorNavigation: false,
showDetectorMenu: false,
selectedDetector: null,
currentSubmenu: null
}),
openNotifications: () => set({
showNotifications: true,
showMonitoring: false,
showFloorNavigation: false,
showListOfDetectors: false,
currentSubmenu: 'notifications',
showDetectorMenu: false,
selectedDetector: null
}),
closeNotifications: () => set({
showNotifications: false,
showNotificationDetectorInfo: false,
selectedNotification: null,
currentSubmenu: null
}),
openListOfDetectors: () => set({
showListOfDetectors: true,
showMonitoring: false,
showFloorNavigation: false,
showNotifications: false,
currentSubmenu: 'detectors',
showDetectorMenu: false,
selectedDetector: null,
showNotificationDetectorInfo: false,
selectedNotification: null
}),
closeListOfDetectors: () => set({
showListOfDetectors: false,
showDetectorMenu: false,
selectedDetector: null,
currentSubmenu: null
}),
openSensors: () => set({
showSensors: true,
showMonitoring: false,
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
currentSubmenu: 'sensors',
showDetectorMenu: false,
selectedDetector: null,
showNotificationDetectorInfo: false,
selectedNotification: null
}),
closeSensors: () => set({
showSensors: false,
showDetectorMenu: false,
selectedDetector: null,
currentSubmenu: null
}),
closeAllMenus: () => {
set({
showMonitoring: false,
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
showSensors: false,
currentSubmenu: null,
});
get().clearSelections();
},
clearSelections: () => set({
selectedDetector: null,
showDetectorMenu: false,
selectedAlert: null,
showAlertMenu: false,
}),
setSelectedDetector: (detector: DetectorType | null) => set({ selectedDetector: detector }),
setShowDetectorMenu: (show: boolean) => set({ showDetectorMenu: show }),
setSelectedNotification: (notification: NotificationType | null) => set({ selectedNotification: notification }),
setShowNotificationDetectorInfo: (show: boolean) => set({ showNotificationDetectorInfo: show }),
setSelectedAlert: (alert: AlertType | null) => set({ selectedAlert: alert }),
setShowAlertMenu: (show: boolean) => set({ showAlertMenu: show }),
isOnNavigationPage: () => {
const { navigationHistory } = get()
const currentRoute = navigationHistory[navigationHistory.length - 1]
return currentRoute === '/navigation'
},
getCurrentRoute: () => {
const { navigationHistory } = get()
return navigationHistory[navigationHistory.length - 1] || null
},
getActiveSidebarItem: () => {
const { showMonitoring, showFloorNavigation, showNotifications, showListOfDetectors, showSensors } = get()
if (showMonitoring) return 3 // Зоны Мониторинга
if (showFloorNavigation) return 4 // Навигация по этажам
if (showNotifications) return 5 // Уведомления
if (showListOfDetectors) return 7 // Список датчиков
if (showSensors) return 8 // Сенсоры
return 2 // Навигация (базовая)
}
}),
{
name: 'navigation-store',
}
)
)
export default useNavigationStore