461 lines
16 KiB
TypeScript
461 lines
16 KiB
TypeScript
'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 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 && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) {
|
||
setCurrentObject(urlObjectId, urlObjectTitle)
|
||
}
|
||
}, [urlObjectId, urlObjectTitle, currentObject.id, 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?.detector_id === detector.detector_id && showDetectorMenu) {
|
||
setShowDetectorMenu(false)
|
||
setSelectedDetector(null)
|
||
} else {
|
||
setSelectedDetector(detector)
|
||
setShowDetectorMenu(true)
|
||
}
|
||
}
|
||
|
||
const closeDetectorMenu = () => {
|
||
setShowDetectorMenu(false)
|
||
setSelectedDetector(null)
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
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) {
|
||
console.log('[NavigationPage] Found detector for alert:', detector)
|
||
|
||
setSelectedAlert(alert)
|
||
setShowAlertMenu(true)
|
||
|
||
setSelectedDetector(detector)
|
||
|
||
console.log('[NavigationPage] Showing AlertMenu for alert:', alert.detector_name)
|
||
} else {
|
||
console.warn('[NavigationPage] Could not find detector for alert:', alert.detector_id)
|
||
}
|
||
}
|
||
|
||
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}
|
||
focusSensorId={selectedDetector?.serial_number ?? selectedAlert?.detector_id?.toString() ?? null}
|
||
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 |