'use client' import React from 'react' import { useRouter } from 'next/navigation' import useNavigationStore from '@/app/store/navigationStore' import AreaChart from '../dashboard/AreaChart' 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 = ({ 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' }) : 'Нет данных' // Данные для графика за последний месяц из реальных notifications const chartData = React.useMemo(() => { const notifications = detector.notifications ?? [] const DAYS_COUNT = 30 // Последний месяц if (notifications.length === 0) { // Если нет уведомлений, возвращаем пустой график return Array.from({ length: DAYS_COUNT }, (_, i) => ({ timestamp: new Date(Date.now() - (DAYS_COUNT - 1 - i) * 24 * 60 * 60 * 1000).toISOString(), critical: 0, warning: 0, })) } // Группируем уведомления по дням за последний месяц const now = new Date() // Устанавливаем время на начало текущего дня для корректного подсчёта now.setHours(0, 0, 0, 0) // Начальная дата: DAYS_COUNT-1 дней назад (чтобы включить текущий день) const startDate = new Date(now.getTime() - (DAYS_COUNT - 1) * 24 * 60 * 60 * 1000) // Создаём карту: дата -> { critical: count, warning: count } const dayMap: Record = {} // Инициализируем все дни нулями (включая текущий день) for (let i = 0; i < DAYS_COUNT; i++) { const date = new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000) const dateKey = date.toISOString().split('T')[0] dayMap[dateKey] = { critical: 0, warning: 0 } } // Подсчитываем уведомления по дням notifications.forEach(notification => { // Детальное логирование для GLE-1 if (detector.serial_number === 'GLE-1') { console.log('[DetectorMenu] Full notification object for GLE-1:', JSON.stringify(notification, null, 2)) } const notifDate = new Date(notification.timestamp) // Проверяем что уведомление попадает в диапазон от startDate до конца текущего дня const endOfToday = new Date(now.getTime() + 24 * 60 * 60 * 1000) if (notifDate >= startDate && notifDate < endOfToday) { const dateKey = notifDate.toISOString().split('T')[0] if (dayMap[dateKey]) { const notifType = String(notification.type || '').toLowerCase() if (notifType === 'critical') { dayMap[dateKey].critical++ } else if (notifType === 'warning') { dayMap[dateKey].warning++ } else { // Если тип не распознан, логируем console.warn('[DetectorMenu] Unknown notification type:', notification.type, 'Full notification:', JSON.stringify(notification, null, 2)) } } } }) // Преобразуем в массив для графика return Object.keys(dayMap) .sort() .map(dateKey => ({ timestamp: dateKey, label: new Date(dateKey).toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' }), critical: dayMap[dateKey].critical, warning: dayMap[dateKey].warning, })) }, [detector.notifications]) // Определение типа детектора и его отображаемого названия const rawDetectorTypeCode = (detector.detector_type || '').toUpperCase() // Извлекаем код из текстового поля type 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 '' } // Fallback: извлекаем код из префикса серийного номера (GLE-3 -> GLE) const deriveCodeFromSerialNumber = (): string => { const serial = (detector.serial_number || '').toUpperCase() if (!serial) return '' // Ищем префикс до дефиса или цифры const match = serial.match(/^([A-Z]+)[-\d]/) return match ? match[1] : '' } // Карта соответствия кодов типов детекторов их русским названиям const detectorTypeLabelMap: Record = { GA: 'Инклинометр', PE: 'Тензометр', GLE: 'Гидроуровень', } // Определяем эффективный код типа датчика с fallback let effectiveDetectorTypeCode = rawDetectorTypeCode // Если rawDetectorTypeCode не найден в карте - используем fallback if (!detectorTypeLabelMap[effectiveDetectorTypeCode]) { effectiveDetectorTypeCode = deriveCodeFromType() || deriveCodeFromSerialNumber() } 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 }) => (
{compact ? ( // Компактный режим: 4 строки по 2 колонки с основной информацией <> {/* Строка 1: Маркировка и тип детектора */}
Маркировка по проекту
{detector.name}
Тип детектора
{displayDetectorTypeLabel}
{/* Строка 2: Местоположение и статус */}
Местоположение
{detector.location}
Статус
{getStatusText(detector.status)}
{/* Строка 3: Временная метка и этаж */}
Временная метка
{formattedTimestamp}
Этаж
{detector.floor}
{/* Строка 4: Серийный номер */}
Серийный номер
{detector.serial_number}
) : ( // Полный режим: 3 строки по 2 колонки с рамками между элементами <> {/* Строка 1: Маркировка по проекту и тип детектора */}
Маркировка по проекту
{detector.name}
Тип детектора
{displayDetectorTypeLabel}
{/* Строка 2: Местоположение и статус */}
Местоположение
{detector.location}
Статус
{getStatusText(detector.status)}
{/* Строка 3: Временная метка и серийный номер */}
Временная метка
{formattedTimestamp}
Серийный номер
{detector.serial_number}
)}
) // Компактный режим с якорной позицией (всплывающее окно) // Используется для отображения информации при наведении на детектор в списке if (compact && anchor) { // Проверяем границы экрана и корректируем позицию const tooltipHeight = 450 // Примерная высота толтипа с графиком const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 800 const bottomOverflow = anchor.top + tooltipHeight - viewportHeight // Если толтип выходит за нижнюю границу, сдвигаем вверх const adjustedTop = bottomOverflow > 0 ? anchor.top - bottomOverflow - 20 : anchor.top return (
{detector.name}
{/* График за последний месяц */}
График за месяц
) } // Полный режим боковой панели (основной режим) // Отображается как правая панель с полной информацией о детекторе return (
{/* Заголовок с названием детектора */}

{detector.name}

{/* Кнопки действий: Отчет и История */}
{/* Секция с детальной информацией о детекторе */} {/* График за последний месяц */}

График за последний месяц

{/* Кнопка закрытия панели */}
) } export default DetectorMenu