Files
aerbim-ht-monitor/frontend/components/navigation/DetectorMenu.tsx
2026-02-02 11:00:40 +03:00

296 lines
14 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 from 'react'
import { useRouter } from 'next/navigation'
import useNavigationStore from '@/app/store/navigationStore'
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 DetectorMenuProps {
detector: DetectorType
isOpen: boolean
onClose: () => void
getStatusText: (status: string) => string
compact?: boolean
anchor?: { left: number; top: number } | null
}
// Главный компонент меню детектора
// Показывает детальную информацию о датчике с возможностью навигации к отчетам и истории
const DetectorMenu: React.FC<DetectorMenuProps> = ({ detector, isOpen, onClose, getStatusText, compact = false, anchor = null }) => {
const router = useRouter()
const { setSelectedDetector, currentObject } = useNavigationStore()
if (!isOpen) return null
// Определение последней временной метки из уведомлений детектора
const latestTimestamp = (() => {
const list = detector.notifications ?? []
if (!Array.isArray(list) || list.length === 0) return null
const dates = list.map(n => new Date(n.timestamp)).filter(d => !isNaN(d.getTime()))
if (dates.length === 0) return null
dates.sort((a, b) => b.getTime() - a.getTime())
return dates[0]
})()
const formattedTimestamp = latestTimestamp
? latestTimestamp.toLocaleString('ru-RU', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
: 'Нет данных'
// Определение типа детектора и его отображаемого названия
const rawDetectorTypeCode = (detector.detector_type || '').toUpperCase()
const deriveCodeFromType = (): string => {
const t = (detector.type || '').toLowerCase()
if (!t) return ''
if (t.includes('инклинометр')) return 'GA'
if (t.includes('тензометр')) return 'PE'
if (t.includes('гидроуров')) return 'GLE'
return ''
}
const effectiveDetectorTypeCode = rawDetectorTypeCode || deriveCodeFromType()
// Карта соответствия кодов типов детекторов их русским названиям
const detectorTypeLabelMap: Record<string, string> = {
GA: 'Инклинометр',
PE: 'Тензометр',
GLE: 'Гидроуровень',
}
const displayDetectorTypeLabel = detectorTypeLabelMap[effectiveDetectorTypeCode] || '—'
// Обработчик клика по кнопке "Отчет" - навигация на страницу отчетов с выбранным детектором
const handleReportsClick = () => {
const currentUrl = new URL(window.location.href)
const objectId = currentUrl.searchParams.get('objectId') || currentObject.id
const objectTitle = currentUrl.searchParams.get('objectTitle') || currentObject.title
const detectorData = {
...detector,
notifications: detector.notifications || []
}
setSelectedDetector(detectorData)
let reportsUrl = '/reports'
const params = new URLSearchParams()
if (objectId) params.set('objectId', objectId)
if (objectTitle) params.set('objectTitle', objectTitle)
if (params.toString()) {
reportsUrl += `?${params.toString()}`
}
router.push(reportsUrl)
}
// Обработчик клика по кнопке "История" - навигация на страницу истории тревог с выбранным детектором
const handleHistoryClick = () => {
const currentUrl = new URL(window.location.href)
const objectId = currentUrl.searchParams.get('objectId') || currentObject.id
const objectTitle = currentUrl.searchParams.get('objectTitle') || currentObject.title
const detectorData = {
...detector,
notifications: detector.notifications || []
}
setSelectedDetector(detectorData)
let alertsUrl = '/alerts'
const params = new URLSearchParams()
if (objectId) params.set('objectId', objectId)
if (objectTitle) params.set('objectTitle', objectTitle)
if (params.toString()) {
alertsUrl += `?${params.toString()}`
}
router.push(alertsUrl)
}
// Компонент секции деталей детектора
// Отображает информацию о датчике в компактном или полном формате
const DetailsSection: React.FC<{ compact?: boolean }> = ({ compact = false }) => (
<div className={compact ? 'mt-2 space-y-1' : 'space-y-0 border border-[rgb(30,31,36)] rounded-lg overflow-hidden'}>
{compact ? (
// Компактный режим: 4 строки по 2 колонки с основной информацией
<>
{/* Строка 1: Маркировка и тип детектора */}
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-[rgb(113,113,122)] text-[11px]">Маркировка по проекту</div>
<div className="text-white text-xs truncate">{detector.name}</div>
</div>
<div>
<div className="text-[rgb(113,113,122)] text-[11px]">Тип детектора</div>
<div className="text-white text-xs truncate">{displayDetectorTypeLabel}</div>
</div>
</div>
{/* Строка 2: Местоположение и статус */}
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-[rgb(113,113,122)] text-[11px]">Местоположение</div>
<div className="text-white text-xs truncate">{detector.location}</div>
</div>
<div>
<div className="text-[rgb(113,113,122)] text-[11px]">Статус</div>
<div className="text-white text-xs truncate">{getStatusText(detector.status)}</div>
</div>
</div>
{/* Строка 3: Временная метка и этаж */}
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-[rgb(113,113,122)] text-[11px]">Временная метка</div>
<div className="text-white text-xs truncate">{formattedTimestamp}</div>
</div>
<div>
<div className="text-[rgb(113,113,122)] text-[11px]">Этаж</div>
<div className="text-white text-xs truncate">{detector.floor}</div>
</div>
</div>
{/* Строка 4: Серийный номер */}
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-[rgb(113,113,122)] text-[11px]">Серийный номер</div>
<div className="text-white text-xs truncate">{detector.serial_number}</div>
</div>
</div>
</>
) : (
// Полный режим: 3 строки по 2 колонки с рамками между элементами
<>
{/* Строка 1: Маркировка по проекту и тип детектора */}
<div className="flex">
<div className="flex-1 p-4 border-r border-[rgb(30,31,36)]">
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Маркировка по проекту</div>
<div className="text-white text-sm">{detector.name}</div>
</div>
<div className="flex-1 p-4">
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Тип детектора</div>
<div className="text-white text-sm">{displayDetectorTypeLabel}</div>
</div>
</div>
{/* Строка 2: Местоположение и статус */}
<div className="flex border-t border-[rgb(30,31,36)]">
<div className="flex-1 p-4 border-r border-[rgb(30,31,36)]">
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Местоположение</div>
<div className="text-white text-sm">{detector.location}</div>
</div>
<div className="flex-1 p-4">
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Статус</div>
<div className="text-white text-sm">{getStatusText(detector.status)}</div>
</div>
</div>
{/* Строка 3: Временная метка и серийный номер */}
<div className="flex border-t border-[rgb(30,31,36)]">
<div className="flex-1 p-4 border-r border-[rgb(30,31,36)]">
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Временная метка</div>
<div className="text-white text-sm">{formattedTimestamp}</div>
</div>
<div className="flex-1 p-4">
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Серийный номер</div>
<div className="text-white text-sm">{detector.serial_number}</div>
</div>
</div>
</>
)}
</div>
)
// Компактный режим с якорной позицией (всплывающее окно)
// Используется для отображения информации при наведении на детектор в списке
if (compact && anchor) {
return (
<div className="absolute z-40" style={{ left: anchor.left, top: anchor.top }}>
<div className="rounded-[10px] bg-black/80 text-white text-xs px-3 py-2 shadow-xl min-w-[300px] max-w-[400px]">
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-semibold truncate">{detector.name}</div>
</div>
<button onClick={onClose} className="text-gray-300 hover:text-white transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="mt-2 grid grid-cols-2 gap-2">
<button onClick={handleReportsClick} className="bg-[#3193f5] hover:bg-[#2563eb] text-white px-2 py-1 rounded-[8px] text-xs font-medium transition-colors flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Отчет
</button>
<button onClick={handleHistoryClick} className="bg-[#3193f5] hover:bg-[#2563eb] text-white px-2 py-1 rounded-[8px] text-xs font-medium transition-colors flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
История
</button>
</div>
<DetailsSection compact={true} />
</div>
</div>
)
}
// Полный режим боковой панели (основной режим)
// Отображается как правая панель с полной информацией о детекторе
return (
<div className="absolute left-[500px] top-0 bg-[#161824] border-r border-gray-700 з-30 w-[454px]" style={{height: 'calc(100% - 73px)', top: '73px'}}>
<div className="h-full overflow-auto p-5">
<div className="flex items-center justify-between mb-4">
{/* Заголовок с названием детектора */}
<h3 className="text-white text-lg font-medium">
{detector.name}
</h3>
{/* Кнопки действий: Отчет и История */}
<div className="flex items-center gap-2">
<button onClick={handleReportsClick} className="bg-[#3193f5] hover:bg-[#2563eb] text-white px-3 py-2 rounded-[10px] text-sm font-medium transition-colors flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Отчет
</button>
<button onClick={handleHistoryClick} className="bg-[#3193f5] hover:bg-[#2563eb] text-white px-3 py-2 rounded-[10px] text-sm font-medium transition-colors flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
История
</button>
</div>
</div>
{/* Секция с детальной информацией о детекторе */}
<DetailsSection />
{/* Кнопка закрытия панели */}
<button
onClick={onClose}
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6л12 12" />
</svg>
</button>
</div>
</div>
)
}
export default DetectorMenu