diff --git a/frontend/app/(protected)/navigation/page.tsx b/frontend/app/(protected)/navigation/page.tsx index 1c02517..6250c82 100644 --- a/frontend/app/(protected)/navigation/page.tsx +++ b/frontend/app/(protected)/navigation/page.tsx @@ -173,7 +173,7 @@ const NavigationPage: React.FC = () => { const urlFocusSensorId = searchParams.get('focusSensorId') const objectId = currentObject.id || urlObjectId const objectTitle = currentObject.title || urlObjectTitle - const [selectedModelPath, setSelectedModelPath] = useState(urlModelPath || '') + const [selectedModelPath, setSelectedModelPath] = useState('') const handleModelLoaded = useCallback(() => { @@ -191,12 +191,8 @@ const NavigationPage: React.FC = () => { 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]); + }, [selectedModelPath]); useEffect(() => { if (urlObjectId && (!currentObject.id || currentObject.id !== urlObjectId)) { @@ -204,12 +200,45 @@ const NavigationPage: React.FC = () => { } }, [urlObjectId, urlObjectTitle, currentObject.id, currentObject.title, setCurrentObject]) - // Восстановление выбранной модели из URL при загрузке страницы + // Восстановление выбранной модели из URL при загрузке страницы (только если переход с focusSensorId) useEffect(() => { - if (urlModelPath && !selectedModelPath) { + // Восстанавливаем modelPath только если есть focusSensorId (переход к конкретному датчику) + if (urlModelPath && urlFocusSensorId && !selectedModelPath) { setSelectedModelPath(urlModelPath); } - }, [urlModelPath, selectedModelPath]) + }, [urlModelPath, urlFocusSensorId, selectedModelPath]) + + // Автоматическая загрузка модели с order=0 при пустом selectedModelPath + useEffect(() => { + const loadDefaultModel = async () => { + // Если модель уже выбрана или нет objectId - пропускаем + if (selectedModelPath || !objectId) return; + + try { + console.log('[NavigationPage] Auto-loading model with order=0 for object:', objectId); + const response = await fetch(`/api/get-zones?object_id=${objectId}`); + const data = await response.json(); + + if (data.success && Array.isArray(data.data)) { + // Сортируем по order и берём первую + const sorted = data.data.slice().sort((a: any, b: any) => { + const oa = typeof a.order === 'number' ? a.order : 0; + const ob = typeof b.order === 'number' ? b.order : 0; + return oa - ob; + }); + + if (sorted.length > 0 && sorted[0].model_path) { + console.log('[NavigationPage] Auto-selected model with order=0:', sorted[0].model_path); + setSelectedModelPath(sorted[0].model_path); + } + } + } catch (error) { + console.error('[NavigationPage] Failed to load default model:', error); + } + }; + + loadDefaultModel(); + }, [selectedModelPath, objectId]) useEffect(() => { const loadDetectors = async () => { diff --git a/frontend/app/(protected)/navigation/page.tsx-копия 4 b/frontend/app/(protected)/navigation/page.tsx-копия 4 deleted file mode 100644 index a5177a8..0000000 --- a/frontend/app/(protected)/navigation/page.tsx-копия 4 +++ /dev/null @@ -1,636 +0,0 @@ -'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) { - 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); - } - } - }; - - // Обработка 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 diff --git a/frontend/app/(protected)/navigation/page.tsx — копия 6 b/frontend/app/(protected)/navigation/page.tsx — копия 6 deleted file mode 100644 index 5264c75..0000000 --- a/frontend/app/(protected)/navigation/page.tsx — копия 6 +++ /dev/null @@ -1,618 +0,0 @@ -'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 diff --git a/frontend/components/model/SceneToolbar.tsx — копия b/frontend/components/model/SceneToolbar.tsx — копия index bd11148..2c7e1ea 100644 --- a/frontend/components/model/SceneToolbar.tsx — копия +++ b/frontend/components/model/SceneToolbar.tsx — копия @@ -21,6 +21,8 @@ interface SceneToolbarProps { onSelectModel?: (modelPath: string) => void; panActive?: boolean; navMenuActive?: boolean; + onToggleSensorHighlights?: () => void; + sensorHighlightsActive?: boolean; } const SceneToolbar: React.FC = ({ @@ -31,6 +33,8 @@ const SceneToolbar: React.FC = ({ onSelectModel, panActive = false, navMenuActive = false, + onToggleSensorHighlights, + sensorHighlightsActive = true, }) => { const [isZoomOpen, setIsZoomOpen] = useState(false); const { showMonitoring, openMonitoring, closeMonitoring, currentZones, loadZones, currentObject } = useNavigationStore(); @@ -43,88 +47,45 @@ const SceneToolbar: React.FC = ({ } }; - const handleHomeClick = async () => { - if (!onSelectModel) return; - - try { - let zones: Zone[] = Array.isArray(currentZones) ? currentZones : []; - - // Если зоны ещё не загружены, откройте Monitoring и загрузите зоны для текущего объекта - if ((!zones || zones.length === 0) && currentObject?.id) { - if (!showMonitoring) { - openMonitoring(); - } - await loadZones(currentObject.id); - zones = useNavigationStore.getState().currentZones || []; - } - - if (!Array.isArray(zones) || zones.length === 0) { - console.warn('No zones available to select a model from.'); - return; - } - - const sorted = zones.slice().sort((a: Zone, b: Zone) => { - const oa = typeof a.order === 'number' ? a.order : 0; - const ob = typeof b.order === 'number' ? b.order : 0; - if (oa !== ob) return oa - ob; - return (a.name || '').localeCompare(b.name || ''); - }); - - const top = sorted[0]; - let chosenPath: string | null = top?.model_path && String(top.model_path).trim() ? top.model_path! : null; - if (!chosenPath) { - const nextWithModel = sorted.find((z) => z.model_path && String(z.model_path).trim()); - chosenPath = nextWithModel?.model_path ?? null; - } - - if (chosenPath) { - onSelectModel(chosenPath); - } else { - console.warn('No zone has a valid model_path to open.'); - } - } catch (error) { - console.error('Error selecting top zone model:', error); - } - }; - const defaultButtons: ToolbarButton[] = [ { icon: '/icons/Zoom.png', - label: 'Zoom', + label: 'Масштаб', onClick: () => setIsZoomOpen(!isZoomOpen), active: isZoomOpen, children: [ { icon: '/icons/plus.svg', - label: 'Zoom In', + label: 'Приблизить', onClick: onZoomIn || (() => {}), }, { icon: '/icons/minus.svg', - label: 'Zoom Out', + label: 'Отдалить', onClick: onZoomOut || (() => {}), }, ] }, { icon: '/icons/Video.png', - label: "Top View", + label: 'Вид сверху', onClick: onTopView || (() => console.log('Top View')), }, { icon: '/icons/Pointer.png', - label: 'Pan', + label: 'Панорамирование', onClick: onPan || (() => console.log('Pan')), active: panActive, }, { - icon: '/icons/Warehouse.png', - label: 'Home', - onClick: handleHomeClick, + icon: '/icons/Eye.png', + label: 'Подсветка датчиков', + onClick: onToggleSensorHighlights || (() => console.log('Toggle Sensor Highlights')), + active: sensorHighlightsActive, }, { icon: '/icons/Layers.png', - label: 'Levels', + label: 'Общий вид', onClick: handleToggleNavMenu, active: navMenuActive, }, diff --git a/frontend/components/model/SceneToolbar.tsx — копия 2 b/frontend/components/model/SceneToolbar.tsx — копия 2 deleted file mode 100644 index 3be2125..0000000 --- a/frontend/components/model/SceneToolbar.tsx — копия 2 +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useState } from 'react'; -import Image from 'next/image'; -import useNavigationStore from '@/app/store/navigationStore'; -import type { Zone } from '@/app/types'; - -interface ToolbarButton { - icon: string; - label: string; - onClick: () => void; - onMouseDown?: () => void; - onMouseUp?: () => void; - active?: boolean; - children?: ToolbarButton[]; -} - -interface SceneToolbarProps { - onZoomIn?: () => void; - onZoomOut?: () => void; - onTopView?: () => void; - onPan?: () => void; - onSelectModel?: (modelPath: string) => void; - panActive?: boolean; - navMenuActive?: boolean; - onToggleSensorHighlights?: () => void; - sensorHighlightsActive?: boolean; -} - -const SceneToolbar: React.FC = ({ - onZoomIn, - onZoomOut, - onTopView, - onPan, - onSelectModel, - panActive = false, - navMenuActive = false, - onToggleSensorHighlights, - sensorHighlightsActive = true, -}) => { - const [isZoomOpen, setIsZoomOpen] = useState(false); - const { showMonitoring, openMonitoring, closeMonitoring, currentZones, loadZones, currentObject } = useNavigationStore(); - - const handleToggleNavMenu = () => { - if (showMonitoring) { - closeMonitoring(); - } else { - openMonitoring(); - } - }; - - const handleHomeClick = async () => { - if (!onSelectModel) return; - - try { - let zones: Zone[] = Array.isArray(currentZones) ? currentZones : []; - - // Если зоны ещё не загружены, откройте Monitoring и загрузите зоны для текущего объекта - if ((!zones || zones.length === 0) && currentObject?.id) { - if (!showMonitoring) { - openMonitoring(); - } - await loadZones(currentObject.id); - zones = useNavigationStore.getState().currentZones || []; - } - - if (!Array.isArray(zones) || zones.length === 0) { - console.warn('No zones available to select a model from.'); - return; - } - - const sorted = zones.slice().sort((a: Zone, b: Zone) => { - const oa = typeof a.order === 'number' ? a.order : 0; - const ob = typeof b.order === 'number' ? b.order : 0; - if (oa !== ob) return oa - ob; - return (a.name || '').localeCompare(b.name || ''); - }); - - const top = sorted[0]; - let chosenPath: string | null = top?.model_path && String(top.model_path).trim() ? top.model_path! : null; - if (!chosenPath) { - const nextWithModel = sorted.find((z) => z.model_path && String(z.model_path).trim()); - chosenPath = nextWithModel?.model_path ?? null; - } - - if (chosenPath) { - onSelectModel(chosenPath); - } else { - console.warn('No zone has a valid model_path to open.'); - } - } catch (error) { - console.error('Error selecting top zone model:', error); - } - }; - - const defaultButtons: ToolbarButton[] = [ - { - icon: '/icons/Zoom.png', - label: 'Масштаб', - onClick: () => setIsZoomOpen(!isZoomOpen), - active: isZoomOpen, - children: [ - { - icon: '/icons/plus.svg', - label: 'Приблизить', - onClick: onZoomIn || (() => {}), - }, - { - icon: '/icons/minus.svg', - label: 'Отдалить', - onClick: onZoomOut || (() => {}), - }, - ] - }, - { - icon: '/icons/Video.png', - label: 'Вид сверху', - onClick: onTopView || (() => console.log('Top View')), - }, - { - icon: '/icons/Pointer.png', - label: 'Панорамирование', - onClick: onPan || (() => console.log('Pan')), - active: panActive, - }, - { - icon: '/icons/Eye.png', - label: 'Подсветка датчиков', - onClick: onToggleSensorHighlights || (() => console.log('Toggle Sensor Highlights')), - active: sensorHighlightsActive, - }, - { - icon: '/icons/Warehouse.png', - label: 'Домой', - onClick: handleHomeClick, - }, - { - icon: '/icons/Layers.png', - label: 'Уровни', - onClick: handleToggleNavMenu, - active: navMenuActive, - }, - ]; - - - return ( -
-
-
- {defaultButtons.map((button, index) => ( -
- - {button.active && button.children && ( -
- {button.children.map((childButton, childIndex) => ( - - ))} -
- )} -
- ))} -
-
-
- ); -}; - -export default SceneToolbar; \ No newline at end of file diff --git a/frontend/components/model/SceneToolbar.tsx — копия 3 b/frontend/components/model/SceneToolbar.tsx — копия 3 deleted file mode 100644 index 2c7e1ea..0000000 --- a/frontend/components/model/SceneToolbar.tsx — копия 3 +++ /dev/null @@ -1,168 +0,0 @@ -import React, { useState } from 'react'; -import Image from 'next/image'; -import useNavigationStore from '@/app/store/navigationStore'; -import type { Zone } from '@/app/types'; - -interface ToolbarButton { - icon: string; - label: string; - onClick: () => void; - onMouseDown?: () => void; - onMouseUp?: () => void; - active?: boolean; - children?: ToolbarButton[]; -} - -interface SceneToolbarProps { - onZoomIn?: () => void; - onZoomOut?: () => void; - onTopView?: () => void; - onPan?: () => void; - onSelectModel?: (modelPath: string) => void; - panActive?: boolean; - navMenuActive?: boolean; - onToggleSensorHighlights?: () => void; - sensorHighlightsActive?: boolean; -} - -const SceneToolbar: React.FC = ({ - onZoomIn, - onZoomOut, - onTopView, - onPan, - onSelectModel, - panActive = false, - navMenuActive = false, - onToggleSensorHighlights, - sensorHighlightsActive = true, -}) => { - const [isZoomOpen, setIsZoomOpen] = useState(false); - const { showMonitoring, openMonitoring, closeMonitoring, currentZones, loadZones, currentObject } = useNavigationStore(); - - const handleToggleNavMenu = () => { - if (showMonitoring) { - closeMonitoring(); - } else { - openMonitoring(); - } - }; - - const defaultButtons: ToolbarButton[] = [ - { - icon: '/icons/Zoom.png', - label: 'Масштаб', - onClick: () => setIsZoomOpen(!isZoomOpen), - active: isZoomOpen, - children: [ - { - icon: '/icons/plus.svg', - label: 'Приблизить', - onClick: onZoomIn || (() => {}), - }, - { - icon: '/icons/minus.svg', - label: 'Отдалить', - onClick: onZoomOut || (() => {}), - }, - ] - }, - { - icon: '/icons/Video.png', - label: 'Вид сверху', - onClick: onTopView || (() => console.log('Top View')), - }, - { - icon: '/icons/Pointer.png', - label: 'Панорамирование', - onClick: onPan || (() => console.log('Pan')), - active: panActive, - }, - { - icon: '/icons/Eye.png', - label: 'Подсветка датчиков', - onClick: onToggleSensorHighlights || (() => console.log('Toggle Sensor Highlights')), - active: sensorHighlightsActive, - }, - { - icon: '/icons/Layers.png', - label: 'Общий вид', - onClick: handleToggleNavMenu, - active: navMenuActive, - }, - ]; - - - return ( -
-
-
- {defaultButtons.map((button, index) => ( -
- - {button.active && button.children && ( -
- {button.children.map((childButton, childIndex) => ( - - ))} -
- )} -
- ))} -
-
-
- ); -}; - -export default SceneToolbar; \ No newline at end of file diff --git a/frontend/components/navigation/Monitoring.tsx b/frontend/components/navigation/Monitoring.tsx index 1cc0150..3164ca8 100644 --- a/frontend/components/navigation/Monitoring.tsx +++ b/frontend/components/navigation/Monitoring.tsx @@ -60,20 +60,12 @@ const Monitoring: React.FC = ({ onClose, onSelectModel }) => { loadZones(objId); }, [currentObject?.id, loadZones]); - // Автоматический выбор модели, если currentModelPath установлен (переход из таблицы) - useEffect(() => { - if (!currentModelPath || autoSelectedRef || !onSelectModel) return; - - console.log('[Monitoring] Auto-selecting model from currentModelPath:', currentModelPath); - setAutoSelectedRef(true); - onSelectModel(currentModelPath); - }, [currentModelPath, autoSelectedRef, onSelectModel]); - // Сброс флага при изменении объекта useEffect(() => { setAutoSelectedRef(false); }, [currentObject?.id]) + // Сортировка зон по order const sortedZones: Zone[] = React.useMemo(() => { const sorted = (currentZones || []).slice().sort((a: Zone, b: Zone) => { const oa = typeof a.order === 'number' ? a.order : 0; @@ -85,6 +77,19 @@ const Monitoring: React.FC = ({ onClose, onSelectModel }) => { return sorted; }, [currentZones]); + // Автоматический выбор модели с order=0 при загрузке зон + useEffect(() => { + // Если уже был автовыбор - пропускаем + if (autoSelectedRef || !onSelectModel) return; + + // Если есть зоны и первая зона (order=0) имеет model_path + if (sortedZones.length > 0 && sortedZones[0]?.model_path) { + console.log('[Monitoring] Auto-selecting model with order=0:', sortedZones[0].model_path); + setAutoSelectedRef(true); + onSelectModel(sortedZones[0].model_path); + } + }, [sortedZones, autoSelectedRef, onSelectModel]) + return (
diff --git a/frontend/components/navigation/Monitoring.tsx — копия 2 b/frontend/components/navigation/Monitoring.tsx — копия 2 index c21fdbb..1cc0150 100644 --- a/frontend/components/navigation/Monitoring.tsx — копия 2 +++ b/frontend/components/navigation/Monitoring.tsx — копия 2 @@ -35,17 +35,22 @@ interface MonitoringProps { } const Monitoring: React.FC = ({ onClose, onSelectModel }) => { - const { currentObject, currentZones, zonesLoading, zonesError, loadZones } = useNavigationStore(); - const [userSelectedModel, setUserSelectedModel] = useState(false); + const { currentObject, currentZones, zonesLoading, zonesError, loadZones, currentModelPath } = useNavigationStore(); + const [autoSelectedRef, setAutoSelectedRef] = React.useState(false); - const handleSelectModel = useCallback((modelPath: string, isUserClick = false) => { - console.log(`[Monitoring] Model selected: ${modelPath}, isUserClick: ${isUserClick}`); + const handleSelectModel = useCallback((modelPath: string) => { + console.log(`[Monitoring] Model selected: ${modelPath}`); console.log(`[Monitoring] onSelectModel callback:`, onSelectModel); - if (isUserClick) { - setUserSelectedModel(true); - } onSelectModel?.(modelPath); - }, [onSelectModel]); + + // Автоматически закрываем панель после выбора модели + if (onClose) { + setTimeout(() => { + console.log('[Monitoring] Auto-closing after model selection'); + onClose(); + }, 100); + } + }, [onSelectModel, onClose]); // Загрузка зон при изменении объекта useEffect(() => { @@ -55,25 +60,19 @@ const Monitoring: React.FC = ({ onClose, onSelectModel }) => { loadZones(objId); }, [currentObject?.id, loadZones]); - // Автоматический выбор первой зоны при загрузке (только если пользователь не выбрал вручную) + // Автоматический выбор модели, если currentModelPath установлен (переход из таблицы) useEffect(() => { - if (userSelectedModel) { - console.log('[Monitoring] User already selected model, skipping auto-selection'); - return; - } - - const sortedZones: Zone[] = (currentZones || []).slice().sort((a: Zone, b: Zone) => { - const oa = typeof a.order === 'number' ? a.order : 0; - const ob = typeof b.order === 'number' ? b.order : 0; - if (oa !== ob) return oa - ob; - return (a.name || '').localeCompare(b.name || ''); - }); - - if (sortedZones.length > 0 && sortedZones[0].model_path && !zonesLoading) { - console.log('[Monitoring] Auto-selecting first zone model'); - handleSelectModel(sortedZones[0].model_path, false); - } - }, [currentZones, zonesLoading, handleSelectModel, userSelectedModel]); + if (!currentModelPath || autoSelectedRef || !onSelectModel) return; + + console.log('[Monitoring] Auto-selecting model from currentModelPath:', currentModelPath); + setAutoSelectedRef(true); + onSelectModel(currentModelPath); + }, [currentModelPath, autoSelectedRef, onSelectModel]); + + // Сброс флага при изменении объекта + useEffect(() => { + setAutoSelectedRef(false); + }, [currentObject?.id]) const sortedZones: Zone[] = React.useMemo(() => { const sorted = (currentZones || []).slice().sort((a: Zone, b: Zone) => { @@ -119,26 +118,30 @@ const Monitoring: React.FC = ({ onClose, onSelectModel }) => { - )} -
- {/* UI зон */} - {zonesError && ( -
- Ошибка загрузки зон: {zonesError} -
- )} - {zonesLoading && ( -
- Загрузка зон... -
- )} - {sortedZones.length > 0 && ( - <> - {sortedZones[0] && ( - - )} - {sortedZones.length > 1 && ( -
- {sortedZones.slice(1).map((zone: Zone, idx: number) => ( - - ))} -
- )} - - )} - {sortedZones.length === 0 && !zonesError && !zonesLoading && ( -
-
- Зоны не найдены для выбранного объекта. Проверьте параметр objectId в API /api/get-zones. -
-
- )} -
- - ); -}; - -export default Monitoring; \ No newline at end of file diff --git a/frontend/components/navigation/Monitoring.tsx — копия 4 b/frontend/components/navigation/Monitoring.tsx — копия 4 deleted file mode 100644 index 1cc0150..0000000 --- a/frontend/components/navigation/Monitoring.tsx — копия 4 +++ /dev/null @@ -1,201 +0,0 @@ -import React, { useEffect, useCallback, useState } from 'react'; -import Image from 'next/image'; -import useNavigationStore from '@/app/store/navigationStore'; -import type { Zone } from '@/app/types'; - -// Безопасный резолвер src изображения, чтобы избежать ошибок Invalid URL в next/image -const resolveImageSrc = (src?: string | null): string => { - if (!src || typeof src !== 'string') return '/images/test_image.png'; - let s = src.trim(); - if (!s) return '/images/test_image.png'; - s = s.replace(/\\/g, '/'); - const lower = s.toLowerCase(); - // Явный плейсхолдер test_image.png маппим на наш статический ресурс - if (lower === 'test_image.png' || lower.endsWith('/test_image.png') || lower.includes('/public/images/test_image.png')) { - return '/images/test_image.png'; - } - // Если путь содержит public/images (даже абсолютный путь ФС), переводим в относительный путь сайта - if (/\/public\/images\//i.test(s)) { - const parts = s.split(/\/public\/images\//i); - const rel = parts[1] || ''; - return `/images/${rel}`; - } - // Абсолютные URL и пути, относительные к сайту - if (s.startsWith('http://') || s.startsWith('https://')) return s; - if (s.startsWith('/')) return s; - // Нормализуем относительные имена ресурсов до путей сайта под /images - // Убираем ведущий 'public/', если он присутствует - s = s.replace(/^public\//i, ''); - return s.startsWith('images/') ? `/${s}` : `/images/${s}`; -} - -interface MonitoringProps { - onClose?: () => void; - onSelectModel?: (modelPath: string) => void; -} - -const Monitoring: React.FC = ({ onClose, onSelectModel }) => { - const { currentObject, currentZones, zonesLoading, zonesError, loadZones, currentModelPath } = useNavigationStore(); - const [autoSelectedRef, setAutoSelectedRef] = React.useState(false); - - const handleSelectModel = useCallback((modelPath: string) => { - console.log(`[Monitoring] Model selected: ${modelPath}`); - console.log(`[Monitoring] onSelectModel callback:`, onSelectModel); - onSelectModel?.(modelPath); - - // Автоматически закрываем панель после выбора модели - if (onClose) { - setTimeout(() => { - console.log('[Monitoring] Auto-closing after model selection'); - onClose(); - }, 100); - } - }, [onSelectModel, onClose]); - - // Загрузка зон при изменении объекта - useEffect(() => { - const objId = currentObject?.id; - if (!objId) return; - console.log(`[Monitoring] Loading zones for object ID: ${objId}`); - loadZones(objId); - }, [currentObject?.id, loadZones]); - - // Автоматический выбор модели, если currentModelPath установлен (переход из таблицы) - useEffect(() => { - if (!currentModelPath || autoSelectedRef || !onSelectModel) return; - - console.log('[Monitoring] Auto-selecting model from currentModelPath:', currentModelPath); - setAutoSelectedRef(true); - onSelectModel(currentModelPath); - }, [currentModelPath, autoSelectedRef, onSelectModel]); - - // Сброс флага при изменении объекта - useEffect(() => { - setAutoSelectedRef(false); - }, [currentObject?.id]) - - const sortedZones: Zone[] = React.useMemo(() => { - const sorted = (currentZones || []).slice().sort((a: Zone, b: Zone) => { - const oa = typeof a.order === 'number' ? a.order : 0; - const ob = typeof b.order === 'number' ? b.order : 0; - if (oa !== ob) return oa - ob; - return (a.name || '').localeCompare(b.name || ''); - }); - console.log(`[Monitoring] Sorted zones:`, sorted.map(z => ({ id: z.id, name: z.name, model_path: z.model_path }))); - return sorted; - }, [currentZones]); - - return ( -
-
-
-

Зоны мониторинга

- {onClose && ( - - )} -
- {/* UI зон */} - {zonesError && ( -
- Ошибка загрузки зон: {zonesError} -
- )} - {zonesLoading && ( -
- Загрузка зон... -
- )} - {sortedZones.length > 0 && ( - <> - {sortedZones[0] && ( - - )} - {sortedZones.length > 1 && ( -
- {sortedZones.slice(1).map((zone: Zone, idx: number) => ( - - ))} -
- )} - - )} - {sortedZones.length === 0 && !zonesError && !zonesLoading && ( -
-
- Зоны не найдены для выбранного объекта. Проверьте параметр objectId в API /api/get-zones. -
-
- )} -
-
- ); -}; - -export default Monitoring; \ No newline at end of file