Files
aerbim-ht-monitor/frontend/app/(protected)/alerts/page.tsx
2025-10-15 19:49:19 +03:00

243 lines
10 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, { useState, useEffect } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import Sidebar from '../../../components/ui/Sidebar'
import useNavigationStore from '../../store/navigationStore'
import DetectorList from '../../../components/alerts/DetectorList'
import ExportMenu from '../../../components/ui/ExportMenu'
import { useSession } from 'next-auth/react'
const AlertsPage: React.FC = () => {
const router = useRouter()
const searchParams = useSearchParams()
const { currentObject, setCurrentObject } = useNavigationStore()
const [selectedDetectors, setSelectedDetectors] = useState<number[]>([])
const { data: session } = useSession()
type AlertItem = {
id: number
type: string
message: string
timestamp: string
acknowledged: boolean
priority: string
detector_id?: number
detector_name?: string
location?: string
object?: string
}
const [alerts, setAlerts] = useState<AlertItem[]>([])
const urlObjectId = searchParams.get('objectId')
const urlObjectTitle = searchParams.get('objectTitle')
const objectId = currentObject.id || urlObjectId
const objectTitle = currentObject.title || urlObjectTitle
useEffect(() => {
if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) {
setCurrentObject(urlObjectId, urlObjectTitle)
}
}, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject])
useEffect(() => {
const loadAlerts = async () => {
try {
const params = new URLSearchParams()
if (currentObject.id) params.set('objectId', currentObject.id)
const res = await fetch(`/api/get-alerts?${params.toString()}`, { cache: 'no-store' })
if (!res.ok) return
const payload = await res.json()
console.log('[AlertsPage] GET /api/get-alerts', {
url: `/api/get-alerts?${params.toString()}`,
status: res.status,
payload,
})
const data = Array.isArray(payload?.data) ? payload.data : (payload?.data?.alerts || [])
console.log('[AlertsPage] parsed alerts:', data)
setAlerts(data as AlertItem[])
} catch (e) {
console.error('Failed to load alerts:', e)
}
}
loadAlerts()
}, [currentObject.id])
const handleBackClick = () => {
router.push('/dashboard')
}
const handleDetectorSelect = (detectorId: number, selected: boolean) => {
if (selected) {
setSelectedDetectors(prev => [...prev, detectorId])
} else {
setSelectedDetectors(prev => prev.filter(id => id !== detectorId))
}
}
const handleExport = async (format: 'csv' | 'pdf', hours: number) => {
try {
const res = await fetch(`/api/get-reports`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(session?.accessToken ? { Authorization: `Bearer ${session.accessToken}` } : {}),
},
body: JSON.stringify({ format: format, hours }),
credentials: 'include',
})
if (!res.ok) {
const errText = await res.text()
throw new Error(`Export failed: ${res.status} ${errText}`)
}
const blob = await res.blob()
const contentDisposition = res.headers.get('Content-Disposition') || res.headers.get('content-disposition')
const filenameMatch = contentDisposition?.match(/filename="(.+)"/)
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace(/T/g, '_')
const filename = filenameMatch ? filenameMatch[1] : `alerts_report_${timestamp}.${format}`
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('Error:', error)
}
}
return (
<div className="flex h-screen bg-[#0e111a]">
<Sidebar
activeItem={8} // История тревог
/>
<div className="flex-1 flex flex-col">
<header className="bg-[#161824] border-b border-gray-700 px-6 py-4">
<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>
</header>
<div className="flex-1 p-6 overflow-auto">
<div className="mb-6">
<div className="flex items-center justify-between mb-6">
<h1 className="text-white text-2xl font-semibold">Уведомления и тревоги</h1>
<div className="flex items-center gap-4">
{selectedDetectors.length > 0 && (
<span className="text-gray-300 text-sm">
Выбрано: <span className="text-white font-medium">{selectedDetectors.length}</span>
</span>
)}
<ExportMenu onExport={handleExport} />
</div>
</div>
<DetectorList
objectId={objectId || undefined}
selectedDetectors={selectedDetectors}
onDetectorSelect={handleDetectorSelect}
/>
{/* История тревог */}
<div className="mt-6 bg-[#161824] rounded-[20px] p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold text-white">История тревог</h2>
<span className="text-sm text-gray-400">Всего: {alerts.length}</span>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-700">
<th className="text-left text-white font-medium py-3">Детектор</th>
<th className="text-left text-white font-medium py-3">Статус</th>
<th className="text-left text-white font-medium py-3">Сообщение</th>
<th className="text-left text-white font-medium py-3">Местоположение</th>
<th className="text-left text-white font-medium py-3">Приоритет</th>
<th className="text-left text-white font-medium py-3">Подтверждено</th>
<th className="text-left text-white font-medium py-3">Время</th>
</tr>
</thead>
<tbody>
{alerts.map((item) => (
<tr key={item.id} className="border-b border-gray-700 hover:bg-gray-800/50 transition-colors">
<td className="py-4">
<div className="text-sm font-medium text-white">{item.detector_name || 'Детектор'}</div>
{item.detector_id ? (
<div className="text-sm text-gray-400">ID: {item.detector_id}</div>
) : null}
</td>
<td className="py-4">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: item.type === 'critical' ? '#b3261e' : item.type === 'warning' ? '#fd7c22' : '#00ff00' }}
></div>
<span className="text-sm text-gray-300">
{item.type === 'critical' ? 'Критический' : item.type === 'warning' ? 'Предупреждение' : 'Информация'}
</span>
</div>
</td>
<td className="py-4">
<div className="text-sm text-white">{item.message}</div>
</td>
<td className="py-4">
<div className="text-sm text-white">{item.location || '-'}</div>
</td>
<td className="py-4">
<span
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-white"
style={{ backgroundColor: item.priority === 'high' ? '#b3261e' : item.priority === 'medium' ? '#fd7c22' : '#00ff00' }}
>
{item.priority === 'high' ? 'Высокий' : item.priority === 'medium' ? 'Средний' : 'Низкий'}
</span>
</td>
<td className="py-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
item.acknowledged ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{item.acknowledged ? 'Да' : 'Нет'}
</span>
</td>
<td className="py-4">
<div className="text-sm text-gray-300">{new Date(item.timestamp).toLocaleString('ru-RU')}</div>
</td>
</tr>
))}
{alerts.length === 0 && (
<tr>
<td colSpan={7} className="py-8 text-center text-gray-400">Записей не найдено</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default AlertsPage