Files
aerbim-ht-monitor/frontend/app/(protected)/alerts/page.tsx
2026-02-02 11:00:40 +03:00

212 lines
8.3 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 AnimatedBackground from '../../../components/ui/AnimatedBackground'
import useNavigationStore from '../../store/navigationStore'
import DetectorList from '../../../components/alerts/DetectorList'
import AlertsList from '../../../components/alerts/AlertsList'
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, selectedDetector } = 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?: string
detector_name?: string
location?: 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])
// Auto-select detector when it comes from navigation store
useEffect(() => {
if (selectedDetector && !selectedDetectors.includes(selectedDetector.detector_id)) {
setSelectedDetectors(prev => [...prev, selectedDetector.detector_id])
}
}, [selectedDetector, selectedDetectors])
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)
console.log('[AlertsPage] Sample alert structure:', data[0])
console.log('[AlertsPage] Alerts with detector_id:', data.filter((alert: any) => alert.detector_id).length)
console.log('[AlertsPage] using transformed alerts:', data)
setAlerts(data)
} 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)
}
}
const handleAcknowledgeToggle = async (alertId: number) => {
try {
const res = await fetch(`/api/update-alert/${alertId}`, { method: 'PATCH' })
const payload = await res.json().catch(() => null)
console.log('[AlertsPage] PATCH /api/update-alert', { id: alertId, status: res.status, payload })
if (!res.ok) {
throw new Error(typeof payload?.error === 'string' ? payload.error : `Update failed (${res.status})`)
}
// Обновить алерты
const params = new URLSearchParams()
if (currentObject.id) params.set('objectId', currentObject.id)
const listRes = await fetch(`/api/get-alerts?${params.toString()}`, { cache: 'no-store' })
const listPayload = await listRes.json().catch(() => null)
const data = Array.isArray(listPayload?.data) ? listPayload.data : (listPayload?.data?.alerts || [])
setAlerts(data as AlertItem[])
} catch (e) {
console.error('Failed to update alert:', e)
}
}
return (
<div className="relative flex h-screen bg-[#0e111a] overflow-hidden">
<AnimatedBackground />
<div className="relative z-20">
<Sidebar
activeItem={8} // История тревог
/>
</div>
<div className="relative z-10 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 style={{ fontFamily: 'Inter, sans-serif', fontWeight: 600 }} className="text-white text-2xl">Уведомления и тревоги</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}
initialSearchTerm={selectedDetector?.detector_id.toString() || ''}
/>
{/* История тревог */}
<div className="mt-8">
<AlertsList
alerts={alerts}
onAcknowledgeToggle={handleAcknowledgeToggle}
initialSearchTerm={selectedDetector?.detector_id.toString() || ''}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default AlertsPage