Files
aerbim-ht-monitor/frontend/app/(protected)/navigation/page.tsx

583 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useEffect, useCallback, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import Sidebar from '../../../components/ui/Sidebar'
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'
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)
useEffect(() => {
if (selectedDetector === null && selectedAlert === null) {
setFocusedSensorId(null);
}
}, [selectedDetector, selectedAlert]);
// Управление выделением всех сенсоров при открытии/закрытии меню Sensors
useEffect(() => {
console.log('[NavigationPage] showSensors changed:', showSensors, 'modelReady:', isModelReady)
if (showSensors && isModelReady) {
// При открытии меню Sensors - выделяем все сенсоры (только если модель готова)
console.log('[NavigationPage] Setting highlightAllSensors to TRUE')
setHighlightAllSensors(true)
setFocusedSensorId(null)
} else if (!showSensors) {
// При закрытии меню Sensors - сбрасываем выделение
console.log('[NavigationPage] Setting highlightAllSensors to FALSE')
setHighlightAllSensors(false)
}
}, [showSensors, isModelReady])
// Дополнительный эффект для задержки выделения сенсоров при открытии меню
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 objectId = currentObject.id || urlObjectId
const objectTitle = currentObject.title || urlObjectTitle
const [selectedModelPath, setSelectedModelPath] = useState<string>('')
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);
}
}, [selectedModelPath]);
useEffect(() => {
if (urlObjectId && (!currentObject.id || currentObject.id !== urlObjectId)) {
setCurrentObject(urlObjectId, urlObjectTitle ?? currentObject.title ?? undefined)
}
}, [urlObjectId, urlObjectTitle, currentObject.id, currentObject.title, setCurrentObject])
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>
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)
// При закрытии меню детектора из Sensors - выделяем все сенсоры снова
if (showSensors) {
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)
// При закрытии меню алерта из Sensors - выделяем все сенсоры снова
if (showSensors) {
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 '#b3261e':
case 'critical':
return 'Критический'
case '#fd7c22':
case 'warning':
return 'Предупреждение'
case '#00ff00':
case 'normal':
return 'Норма'
default:
return 'Неизвестно'
}
}
return (
<div className="flex h-screen bg-[#0e111a]">
<Sidebar
activeItem={2}
/>
<div className="flex-1 flex flex-col relative">
{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={isModelReady && !modelError}
/>
{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={isModelReady && !modelError}
/>
{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 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
<div className="text-gray-400 text-lg font-semibold mb-4">
3D модель не загружена
</div>
<div className="text-gray-300 mb-4">
Модель не готова к отображению
</div>
<div className="text-sm text-gray-400">
Выберите модель из навигации по этажам
</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}
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