346 lines
14 KiB
TypeScript
346 lines
14 KiB
TypeScript
'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
|