Files
aerbim-ht-monitor/frontend/components/navigation/AlertMenu.tsx
2026-01-21 03:16:52 +03:00

346 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'
import AreaChart from '../dashboard/AreaChart'
import * as statusColors from '../../lib/statusColors'
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
}
interface AlertMenuProps {
alert: AlertType
isOpen: boolean
onClose: () => void
getStatusText: (status: string) => string
compact?: boolean
anchor?: { left: number; top: number } | null
}
const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatusText, compact = false, anchor = null }) => {
const router = useRouter()
const { setSelectedDetector, currentObject } = useNavigationStore()
if (!isOpen) return null
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const getPriorityText = (priority: string) => {
if (typeof priority !== 'string') return 'Неизвестно';
switch (priority.toLowerCase()) {
case 'high': return 'Высокий'
case 'medium': return 'Средний'
case 'low': return 'Низкий'
default: return priority
}
}
const getStatusColorCircle = (status: string) => {
if (status === statusColors.STATUS_COLOR_CRITICAL) return 'bg-red-500'
if (status === statusColors.STATUS_COLOR_WARNING) return 'bg-orange-500'
if (status === statusColors.STATUS_COLOR_NORMAL) return 'bg-green-500'
switch (status?.toLowerCase()) {
case 'critical':
return 'bg-red-500'
case 'warning':
return 'bg-orange-500'
case 'normal':
return 'bg-green-500'
default:
return 'bg-gray-500'
}
}
const getTimeAgo = (timestamp: string) => {
const now = new Date()
const alertTime = new Date(timestamp)
const diffMs = now.getTime() - alertTime.getTime()
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
const diffMonths = Math.floor(diffDays / 30)
if (diffMonths > 0) {
return `${diffMonths} ${diffMonths === 1 ? 'месяц' : diffMonths < 5 ? 'месяца' : 'месяцев'}`
} else if (diffDays > 0) {
return `${diffDays} ${diffDays === 1 ? 'день' : diffDays < 5 ? 'дня' : 'дней'}`
} else {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
if (diffHours > 0) {
return `${diffHours} ${diffHours === 1 ? 'час' : diffHours < 5 ? 'часа' : 'часов'}`
} else {
const diffMinutes = Math.floor(diffMs / (1000 * 60))
return `${diffMinutes} ${diffMinutes === 1 ? 'минута' : diffMinutes < 5 ? 'минуты' : 'минут'}`
}
}
}
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_id: alert.detector_id,
name: alert.detector_name,
serial_number: '',
object: alert.object || '',
status: '',
type: '',
detector_type: '',
location: alert.location || '',
floor: 0,
checked: false,
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_id: alert.detector_id,
name: alert.detector_name,
serial_number: '',
object: alert.object || '',
status: '',
type: '',
detector_type: '',
location: alert.location || '',
floor: 0,
checked: false,
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 chartData: { timestamp: string; value: number }[] = [
{ timestamp: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(), value: 75 },
{ timestamp: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(), value: 82 },
{ timestamp: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString(), value: 78 },
{ timestamp: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), value: 85 },
{ timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), value: 90 },
{ timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), value: 88 },
{ timestamp: new Date().toISOString(), value: 92 }
]
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-4 py-3 shadow-xl min-w-[420px] max-w-[454px]">
<div className="flex items-start justify-between gap-4 mb-3">
<div className="flex-1">
<div className="font-semibold truncate text-base">{alert.detector_name}</div>
</div>
<div className="flex gap-2">
<button onClick={handleReportsClick} className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-4 py-2 rounded-[6px] text-sm font-medium transition-colors flex items-center gap-2 flex-1">
<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-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-4 py-2 rounded-[6px] text-sm font-medium transition-colors flex items-center gap-2 flex-1">
<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>
<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>
{/* Тело: 3 строки / 2 колонки */}
<div className="space-y-2 mb-6">
{/* Строка 1: Статус */}
<div className="grid grid-cols-2 gap-3">
<div className="text-[rgb(113,113,122)] text-xs">Статус</div>
<div className="flex items-center gap-1 justify-end">
<div className={`w-2 h-2 rounded-full ${getStatusColorCircle(alert.status)}`}></div>
<div className="text-white text-xs">{getStatusText(alert.status)}</div>
</div>
</div>
{/* Строка 2: Причина тревоги */}
<div className="grid grid-cols-2 gap-3">
<div className="text-[rgb(113,113,122)] text-xs">Причина тревоги</div>
<div className="text-white text-xs truncate">{alert.message}</div>
</div>
{/* Строка 3: Временная метка */}
<div className="grid grid-cols-2 gap-3">
<div className="text-[rgb(113,113,122)] text-xs">Временная метка</div>
<div className="text-white text-xs text-right">{getTimeAgo(alert.timestamp)}</div>
</div>
</div>
{/* Добавлен раздел дата/время и график для компактного режима */}
<div className="space-y-6">
<div className="text-[rgb(113,113,122)] text-xs text-center">{formatDate(alert.timestamp)}</div>
<div className="grid grid-cols-1 gap-6 min-h-[70px]">
<AreaChart data={chartData} />
</div>
</div>
</div>
</div>
)
}
return (
<div className="absolute left-[500px] top-0 bg-[#161824] border-r border-gray-700 z-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">
{alert.detector_name}
</h3>
<button
onClick={onClose}
className="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 6l12 12" />
</svg>
</button>
</div>
{/* Тело: Две колонки - Три строки */}
<div className="grid grid-cols-2 gap-16 mb-12 flex-grow">
{/* Колонка 1 - Строка 1: Статус */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Статус</div>
<div className="flex items-center gap-2 justify-end">
<div className={`w-3 h-3 rounded-full ${getStatusColorCircle(alert.status)}`}></div>
<span className="text-white text-sm">{getStatusText(alert.status)}</span>
</div>
</div>
{/* Колонка 1 - Строка 2: Причина тревоги */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Причина тревоги</div>
<div className="text-white text-sm">{alert.message}</div>
</div>
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Причина тревоги</div>
<div className="text-white text-sm">{alert.message}</div>
</div>
{/* Колонка 1 - Строка 3: Временная метка */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Временная метка</div>
<div className="text-white text-sm">{getTimeAgo(alert.timestamp)}</div>
</div>
{/* Колонка 2 - Строка 1: Приоритет */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Приоритет</div>
<div className="text-white text-sm text-right">{getPriorityText(alert.priority)}</div>
</div>
{/* Колонка 2 - Строка 2: Локация */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Локация</div>
<div className="text-white text-sm text-right">{alert.location}</div>
</div>
{/* Колонка 2 - Строка 3: Объект */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Объект</div>
<div className="text-white text-sm text-right">{alert.object}</div>
</div>
{/* Колонка 2 - Строка 4: Отчет */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">Отчет</div>
<div className="text-white text-sm text-right">Доступен</div>
</div>
{/* Колонка 2 - Строка 5: История */}
<div className="space-y-2">
<div className="text-[rgb(113,113,122)] text-sm">История</div>
<div className="text-white text-sm text-right">Просмотр</div>
</div>
</div>
{/* Низ: Две строки - первая содержит дату/время, вторая строка ниже - наш график */}
<div className="space-y-8 flex-shrink-0">
{/* Строка 1: Дата/время */}
<div className="p-8 bg-[rgb(22,24,36)] rounded-lg">
<div className="text-[rgb(113,113,122)] text-base font-medium mb-4">Дата/время</div>
<div className="text-white text-base">{formatDate(alert.timestamp)}</div>
</div>
{/* Charts */}
<div className="grid grid-cols-1 gap-6 min-h-[300px]">
<div className="p-2 min-h-[30px]">
<AreaChart data={chartData} />
</div>
</div>
</div>
<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 6l12 12" />
</svg>
</button>
</div>
</div>
)
}
export default AlertMenu