'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(() => import('../../../components/model/ModelViewer'), { ssr: false, loading: () => (
Загрузка 3D-модуля…
), }) 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, showSensorHighlights, toggleSensorHighlights } = useNavigationStore() const [detectorsData, setDetectorsData] = useState<{ detectors: Record }>({ detectors: {} }) const [detectorsError, setDetectorsError] = useState(null) const [modelError, setModelError] = useState(null) const [isModelReady, setIsModelReady] = useState(false) const [focusedSensorId, setFocusedSensorId] = useState(null) const [highlightAllSensors, setHighlightAllSensors] = useState(false) const sensorStatusMap = React.useMemo(() => { // Создаём карту статусов всегда для отображения цветов датчиков const map: Record = {} 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 urlFocusSensorId = searchParams.get('focusSensorId') const objectId = currentObject.id || urlObjectId const objectTitle = currentObject.title || urlObjectTitle const [selectedModelPath, setSelectedModelPath] = useState(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 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) { // Всегда показываем меню детектора для всех датчиков 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); } } }; // Обработка focusSensorId из URL (при переходе из таблиц событий) useEffect(() => { if (urlFocusSensorId && isModelReady && detectorsData) { console.log('[NavigationPage] Setting focusSensorId from URL:', urlFocusSensorId) setFocusedSensorId(urlFocusSensorId) setHighlightAllSensors(false) // Автоматически открываем тултип датчика setTimeout(() => { handleSensorSelection(urlFocusSensorId) }, 500) // Задержка для полной инициализации // Очищаем URL от параметра после применения const newUrl = new URL(window.location.href) newUrl.searchParams.delete('focusSensorId') window.history.replaceState({}, '', newUrl.toString()) } }, [urlFocusSensorId, isModelReady, detectorsData]) 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 (
{showMonitoring && (
{ console.log('[NavigationPage] Model selected:', path); setSelectedModelPath(path) setModelError(null) setIsModelReady(false) }} />
)} {showFloorNavigation && (
)} {showNotifications && (
{detectorsError && (
{detectorsError}
)}
)} {showListOfDetectors && (
{detectorsError && (
{detectorsError}
)}
)} {showSensors && (
{detectorsError && (
{detectorsError}
)}
)} {showNotifications && showNotificationDetectorInfo && selectedNotification && (() => { const detectorData = Object.values(detectorsData.detectors).find( detector => detector.detector_id === selectedNotification.detector_id ); return detectorData ? (
) : null; })()} {showFloorNavigation && showDetectorMenu && selectedDetector && ( null )}
{modelError ? ( <> {console.log('[NavigationPage] Rendering error message, modelError:', modelError)}
Ошибка загрузки 3D модели
{modelError}
Используйте навигацию по этажам для просмотра детекторов
) : !selectedModelPath ? (
AerBIM HT Monitor
Выберите модель для отображения
) : ( { 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={showSensorHighlights && highlightAllSensors} sensorStatusMap={sensorStatusMap} isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors} onSensorPick={handleSensorSelection} renderOverlay={({ anchor }) => ( <> {selectedAlert && showAlertMenu && anchor ? ( ) : selectedDetector && showDetectorMenu && anchor ? ( ) : null} )} /> )}
) } export default NavigationPage