Files
aerbim-ht-monitor/frontend/app/(protected)/navigation/page.tsx
2025-11-11 10:07:38 +03:00

334 lines
12 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 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
}
const NavigationPage: React.FC = () => {
const router = useRouter()
const searchParams = useSearchParams()
const {
currentObject,
setCurrentObject,
showMonitoring,
showFloorNavigation,
showNotifications,
selectedDetector,
showDetectorMenu,
selectedNotification,
showNotificationDetectorInfo,
closeMonitoring,
closeFloorNavigation,
closeNotifications,
setSelectedDetector,
setShowDetectorMenu,
setSelectedNotification,
setShowNotificationDetectorInfo
} = 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('Model loading error:', error)
setModelError(error)
setIsModelReady(false)
}, [])
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 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
objectId={objectId || undefined}
onClose={closeMonitoring}
onSelectModel={(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>
)}
{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 ? (
<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>
) : (
<ModelViewer
modelPath={selectedModelPath}
onModelLoaded={handleModelLoaded}
onError={handleModelError}
focusSensorId={selectedDetector?.serial_number ?? null}
renderOverlay={({ 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