From 79e4845870998c9460c7f8638cd1712b09b41a85 Mon Sep 17 00:00:00 2001 From: sysadminix Date: Tue, 3 Feb 2026 21:46:20 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D0=B8=D1=87=D0=B5=D1=81=D1=82=D0=B0=20=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B5=D0=B9?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D0=B5=20=D0=98=D1=81=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20=D1=82?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D0=BE=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/alerts/DetectorList.tsx | 30 +- .../alerts/DetectorList.tsx — копия 2 | 30 -- frontend/components/dashboard/AreaChart.tsx | 82 +++- .../dashboard/AreaChart.tsx — копия | 196 ++++++++++ frontend/components/dashboard/BarChart.tsx | 128 ++++-- .../dashboard/BarChart.tsx — копия | 200 ++++++++++ frontend/components/dashboard/Dashboard.tsx | 10 +- .../dashboard/Dashboard.tsx — копия 3 | 368 ++++++++++++++++++ frontend/lib/chartDataAggregator.ts | 94 +++++ .../chartDataAggregator — копия.ts | 160 ++++++++ 10 files changed, 1209 insertions(+), 89 deletions(-) create mode 100644 frontend/components/dashboard/AreaChart.tsx — копия create mode 100644 frontend/components/dashboard/BarChart.tsx — копия create mode 100644 frontend/components/dashboard/Dashboard.tsx — копия 3 create mode 100644 frontend/lib/chartDataAggregator — копия.ts diff --git a/frontend/components/alerts/DetectorList.tsx b/frontend/components/alerts/DetectorList.tsx index 0027a0a..684d09a 100644 --- a/frontend/components/alerts/DetectorList.tsx +++ b/frontend/components/alerts/DetectorList.tsx @@ -68,7 +68,7 @@ const DetectorList: React.FC = ({ objectId, selectedDetectors const [detectors, setDetectors] = useState([]) const [searchTerm, setSearchTerm] = useState(initialSearchTerm) const [currentPage, setCurrentPage] = useState(1) - const itemsPerPage = 20 + const [itemsPerPage, setItemsPerPage] = useState(10) useEffect(() => { const loadDetectors = async () => { @@ -80,6 +80,7 @@ const DetectorList: React.FC = ({ objectId, selectedDetectors const rawArray: RawDetector[] = Object.values(detectorsData).filter( (detector) => (objectId ? detector.object === objectId : true) ) + const normalized: Detector[] = rawArray.map((d) => ({ detector_id: d.detector_id, name: d.name, @@ -89,7 +90,8 @@ const DetectorList: React.FC = ({ objectId, selectedDetectors floor: d.floor, checked: false, })) - console.log('[DetectorList] Payload:', payload) + console.log('[DetectorList] Payload:', payload) + console.log('[DetectorList] Total detectors:', normalized.length) setDetectors(normalized) } catch (e) { console.error('Failed to load detectors:', e) @@ -129,6 +131,30 @@ const DetectorList: React.FC = ({ objectId, selectedDetectors
+ {/* Выбор количества элементов */} +
+ Показывать: +
+ + + + +
+
+
= ({ objectId, selectedDetectors - @@ -185,14 +163,6 @@ const DetectorList: React.FC = ({ objectId, selectedDetectors return ( -
- 0} - onChange={(e) => { - if (e.target.checked) { - currentDetectors.forEach(detector => { - if (!selectedDetectors.includes(detector.detector_id)) { - onDetectorSelect(detector.detector_id, true) - } - }) - } else { - currentDetectors.forEach(detector => { - if (selectedDetectors.includes(detector.detector_id)) { - onDetectorSelect(detector.detector_id, false) - } - }) - } - }} - className="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2" - /> - Детектор Статус Местоположение
- onDetectorSelect(detector.detector_id, e.target.checked)} - className="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2" - /> - {detector.name}
diff --git a/frontend/components/dashboard/AreaChart.tsx b/frontend/components/dashboard/AreaChart.tsx index a362a24..615ace4 100644 --- a/frontend/components/dashboard/AreaChart.tsx +++ b/frontend/components/dashboard/AreaChart.tsx @@ -3,7 +3,8 @@ import React from 'react' interface ChartDataPoint { - value: number + critical?: number + warning?: number label?: string timestamp?: string } @@ -23,19 +24,30 @@ const AreaChart: React.FC = ({ className = '', data }) => { const safeData = (Array.isArray(data) && data.length > 0) ? data - : Array.from({ length: 7 }, () => ({ value: 0 })) + : Array.from({ length: 7 }, () => ({ critical: 0, warning: 0 })) - const maxVal = Math.max(...safeData.map(d => d.value || 0), 1) + const maxVal = Math.max(...safeData.map(d => Math.max(d.critical || 0, d.warning || 0)), 1) const stepX = safeData.length > 1 ? plotWidth / (safeData.length - 1) : plotWidth - const points = safeData.map((d, i) => { + // Точки для критических событий (красная линия) + const criticalPoints = safeData.map((d, i) => { const x = margin.left + i * stepX - const y = baselineY - (Math.min(d.value || 0, maxVal) / maxVal) * plotHeight + const y = baselineY - (Math.min(d.critical || 0, maxVal) / maxVal) * plotHeight return { x, y } }) - const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') - const areaPath = `${linePath} L${width - margin.right},${baselineY} L${margin.left},${baselineY} Z` + // Точки для предупреждений (оранжевая линия) + const warningPoints = safeData.map((d, i) => { + const x = margin.left + i * stepX + const y = baselineY - (Math.min(d.warning || 0, maxVal) / maxVal) * plotHeight + return { x, y } + }) + + const criticalLinePath = criticalPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + const criticalAreaPath = `${criticalLinePath} L${width - margin.right},${baselineY} L${margin.left},${baselineY} Z` + + const warningLinePath = warningPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + const warningAreaPath = `${warningLinePath} L${width - margin.right},${baselineY} L${margin.left},${baselineY} Z` // Генерируем Y-оси метки const ySteps = 4 @@ -59,9 +71,13 @@ const AreaChart: React.FC = ({ className = '', data }) => {
- - - + + + + + + + @@ -157,7 +173,7 @@ const AreaChart: React.FC = ({ className = '', data }) => { fontFamily="Arial, sans-serif" transform={`rotate(-90, 20, ${margin.top + plotHeight / 2})`} > - Значение + Количество {/* Подпись оси X */} @@ -172,22 +188,52 @@ const AreaChart: React.FC = ({ className = '', data }) => { Время - {/* График */} - - + {/* График предупреждений (оранжевый) - рисуем первым чтобы был на заднем плане */} + + - {/* Точки данных */} - {points.map((p, i) => ( + {/* Точки данных для предупреждений */} + {warningPoints.map((p, i) => ( ))} + + {/* График критических событий (красный) - рисуем поверх */} + + + + {/* Точки данных для критических */} + {criticalPoints.map((p, i) => ( + + ))} + + {/* Легенда */} + + + + Критические + + + + + Предупреждения + +
) diff --git a/frontend/components/dashboard/AreaChart.tsx — копия b/frontend/components/dashboard/AreaChart.tsx — копия new file mode 100644 index 0000000..a362a24 --- /dev/null +++ b/frontend/components/dashboard/AreaChart.tsx — копия @@ -0,0 +1,196 @@ +'use client' + +import React from 'react' + +interface ChartDataPoint { + value: number + label?: string + timestamp?: string +} + +interface AreaChartProps { + className?: string + data?: ChartDataPoint[] +} + +const AreaChart: React.FC = ({ className = '', data }) => { + const width = 635 + const height = 280 + const margin = { top: 20, right: 30, bottom: 50, left: 60 } + const plotWidth = width - margin.left - margin.right + const plotHeight = height - margin.top - margin.bottom + const baselineY = margin.top + plotHeight + + const safeData = (Array.isArray(data) && data.length > 0) + ? data + : Array.from({ length: 7 }, () => ({ value: 0 })) + + const maxVal = Math.max(...safeData.map(d => d.value || 0), 1) + const stepX = safeData.length > 1 ? plotWidth / (safeData.length - 1) : plotWidth + + const points = safeData.map((d, i) => { + const x = margin.left + i * stepX + const y = baselineY - (Math.min(d.value || 0, maxVal) / maxVal) * plotHeight + return { x, y } + }) + + const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + const areaPath = `${linePath} L${width - margin.right},${baselineY} L${margin.left},${baselineY} Z` + + // Генерируем Y-оси метки + const ySteps = 4 + const yLabels = Array.from({ length: ySteps + 1 }, (_, i) => { + const value = (maxVal / ySteps) * (ySteps - i) + const y = margin.top + (i * plotHeight) / ySteps + return { value: value.toFixed(1), y } + }) + + // Генерируем X-оси метки (показываем каждую 2-ю или 3-ю точку) + const xLabelStep = Math.ceil(safeData.length / 5) + const xLabels = safeData + .map((d, i) => { + const x = margin.left + i * stepX + const label = d.label || d.timestamp || `${i + 1}` + return { label, x, index: i } + }) + .filter((_, i) => i % xLabelStep === 0 || i === safeData.length - 1) + + return ( +
+ + + + + + + + + {/* Сетка Y */} + {yLabels.map((label, i) => ( + + ))} + + {/* Ось X */} + + + {/* Ось Y */} + + + {/* Y-оси метки и подписи */} + {yLabels.map((label, i) => ( + + + + {label.value} + + + ))} + + {/* X-оси метки и подписи */} + {xLabels.map((label, i) => ( + + + + {typeof label.label === 'string' ? label.label.substring(0, 10) : `${label.index + 1}`} + + + ))} + + {/* Подпись оси Y */} + + Значение + + + {/* Подпись оси X */} + + Время + + + {/* График */} + + + + {/* Точки данных */} + {points.map((p, i) => ( + + ))} + +
+ ) +} + +export default AreaChart diff --git a/frontend/components/dashboard/BarChart.tsx b/frontend/components/dashboard/BarChart.tsx index 727c837..c9ebdd7 100644 --- a/frontend/components/dashboard/BarChart.tsx +++ b/frontend/components/dashboard/BarChart.tsx @@ -3,9 +3,9 @@ import React from 'react' interface ChartDataPoint { - value: number + critical?: number + warning?: number label?: string - color?: string } interface BarChartProps { @@ -22,10 +22,14 @@ const BarChart: React.FC = ({ className = '', data }) => { const baselineY = margin.top + plotHeight const barData = (Array.isArray(data) && data.length > 0) - ? data.map(d => ({ value: d.value, label: d.label || '', color: d.color || 'rgb(37, 99, 235)' })) - : Array.from({ length: 12 }, (_, i) => ({ value: 0, label: `${i + 1}`, color: 'rgb(37, 99, 235)' })) + ? data.map(d => ({ + critical: d.critical || 0, + warning: d.warning || 0, + label: d.label || '' + })) + : Array.from({ length: 12 }, (_, i) => ({ critical: 0, warning: 0, label: `${i + 1}` })) - const maxVal = Math.max(...barData.map(b => b.value || 0), 1) + const maxVal = Math.max(...barData.map(b => (b.critical || 0) + (b.warning || 0)), 1) // Генерируем Y-оси метки const ySteps = 4 @@ -141,7 +145,7 @@ const BarChart: React.FC = ({ className = '', data }) => { fontFamily="Arial, sans-serif" transform={`rotate(-90, 20, ${margin.top + plotHeight / 2})`} > - Значение + Количество {/* Подпись оси X */} @@ -156,42 +160,98 @@ const BarChart: React.FC = ({ className = '', data }) => { Период - {/* Столбцы */} + {/* Столбцы - сгруппированные по критическим и предупреждениям */} {barData.map((bar, index) => { const barWidth = Math.max(30, plotWidth / barData.length - 8) const barSpacing = (plotWidth - barWidth * barData.length) / (barData.length - 1 || 1) - const x = margin.left + index * (barWidth + barSpacing) - const barHeight = (bar.value / maxVal) * plotHeight - const y = baselineY - barHeight + const groupX = margin.left + index * (barWidth + barSpacing) + + const totalValue = (bar.critical || 0) + (bar.warning || 0) + + // Ширина каждого подстолбца + const subBarWidth = barWidth / 2 - 2 + + // Критический столбец (красный) + const criticalHeight = (bar.critical / maxVal) * plotHeight + const criticalY = baselineY - criticalHeight + + // Предупреждение столбец (оранжевый) + const warningHeight = (bar.warning / maxVal) * plotHeight + const warningY = baselineY - warningHeight return ( - - - {/* Тень для глубины */} - + + {/* Критический столбец */} + {bar.critical > 0 && ( + <> + + + + )} + + {/* Предупреждение столбец */} + {bar.warning > 0 && ( + <> + + + + )} ) })} + + {/* Легенда */} + + + + Критические + + + + + Предупреждения + +
) diff --git a/frontend/components/dashboard/BarChart.tsx — копия b/frontend/components/dashboard/BarChart.tsx — копия new file mode 100644 index 0000000..727c837 --- /dev/null +++ b/frontend/components/dashboard/BarChart.tsx — копия @@ -0,0 +1,200 @@ +'use client' + +import React from 'react' + +interface ChartDataPoint { + value: number + label?: string + color?: string +} + +interface BarChartProps { + className?: string + data?: ChartDataPoint[] +} + +const BarChart: React.FC = ({ className = '', data }) => { + const width = 635 + const height = 280 + const margin = { top: 20, right: 30, bottom: 50, left: 60 } + const plotWidth = width - margin.left - margin.right + const plotHeight = height - margin.top - margin.bottom + const baselineY = margin.top + plotHeight + + const barData = (Array.isArray(data) && data.length > 0) + ? data.map(d => ({ value: d.value, label: d.label || '', color: d.color || 'rgb(37, 99, 235)' })) + : Array.from({ length: 12 }, (_, i) => ({ value: 0, label: `${i + 1}`, color: 'rgb(37, 99, 235)' })) + + const maxVal = Math.max(...barData.map(b => b.value || 0), 1) + + // Генерируем Y-оси метки + const ySteps = 4 + const yLabels = Array.from({ length: ySteps + 1 }, (_, i) => { + const value = (maxVal / ySteps) * (ySteps - i) + const y = margin.top + (i * plotHeight) / ySteps + return { value: value.toFixed(1), y } + }) + + // Генерируем X-оси метки (показываем каждую 2-ю или 3-ю) + const xLabelStep = Math.ceil(barData.length / 8) + const xLabels = barData + .map((d, i) => { + const barWidth = Math.max(30, plotWidth / barData.length - 8) + const barSpacing = (plotWidth - barWidth * barData.length) / (barData.length - 1 || 1) + const x = margin.left + i * (barWidth + barSpacing) + barWidth / 2 + return { label: d.label || `${i + 1}`, x, index: i } + }) + .filter((_, i) => i % xLabelStep === 0 || i === barData.length - 1) + + return ( +
+ + {/* Сетка Y */} + {yLabels.map((label, i) => ( + + ))} + + {/* Ось X */} + + + {/* Ось Y */} + + + {/* Y-оси метки и подписи */} + {yLabels.map((label, i) => ( + + + + {label.value} + + + ))} + + {/* X-оси метки и подписи */} + {xLabels.map((label, i) => ( + + + + {typeof label.label === 'string' ? label.label.substring(0, 8) : `${label.index + 1}`} + + + ))} + + {/* Подпись оси Y */} + + Значение + + + {/* Подпись оси X */} + + Период + + + {/* Столбцы */} + {barData.map((bar, index) => { + const barWidth = Math.max(30, plotWidth / barData.length - 8) + const barSpacing = (plotWidth - barWidth * barData.length) / (barData.length - 1 || 1) + const x = margin.left + index * (barWidth + barSpacing) + const barHeight = (bar.value / maxVal) * plotHeight + const y = baselineY - barHeight + + return ( + + + {/* Тень для глубины */} + + + ) + })} + +
+ ) +} + +export default BarChart diff --git a/frontend/components/dashboard/Dashboard.tsx b/frontend/components/dashboard/Dashboard.tsx index 6559533..c7e144e 100644 --- a/frontend/components/dashboard/Dashboard.tsx +++ b/frontend/components/dashboard/Dashboard.tsx @@ -8,7 +8,7 @@ import useNavigationStore from '../../app/store/navigationStore' import ChartCard from './ChartCard' import AreaChart from './AreaChart' import BarChart from './BarChart' -import { aggregateChartDataByDays } from '../../lib/chartDataAggregator' +import { aggregateChartDataByDays, aggregateAlertsBySeverity } from '../../lib/chartDataAggregator' const Dashboard: React.FC = () => { const router = useRouter() @@ -153,10 +153,10 @@ const Dashboard: React.FC = () => { setSelectedTablePeriod(period) } - // Агрегируем данные графика в зависимости от периода + // Агрегируем данные графика в зависимости от периода с разделением по severity const chartData = useMemo(() => { - return aggregateChartDataByDays(rawChartData, selectedChartPeriod) - }, [rawChartData, selectedChartPeriod]) + return aggregateAlertsBySeverity(dashboardAlerts, selectedChartPeriod) + }, [dashboardAlerts, selectedChartPeriod]) const interSemiboldStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 600 } const interRegularStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 400 } @@ -249,7 +249,7 @@ const Dashboard: React.FC = () => { - ({ value: d.value, label: d.label }))} /> + diff --git a/frontend/components/dashboard/Dashboard.tsx — копия 3 b/frontend/components/dashboard/Dashboard.tsx — копия 3 new file mode 100644 index 0000000..6559533 --- /dev/null +++ b/frontend/components/dashboard/Dashboard.tsx — копия 3 @@ -0,0 +1,368 @@ +'use client' + +import React, { useEffect, useState, useMemo } from 'react' +import { useRouter } from 'next/navigation' +import Sidebar from '../ui/Sidebar' +import AnimatedBackground from '../ui/AnimatedBackground' +import useNavigationStore from '../../app/store/navigationStore' +import ChartCard from './ChartCard' +import AreaChart from './AreaChart' +import BarChart from './BarChart' +import { aggregateChartDataByDays } from '../../lib/chartDataAggregator' + +const Dashboard: React.FC = () => { + const router = useRouter() + const { currentObject, setCurrentSubmenu, closeMonitoring, closeFloorNavigation, closeNotifications, navigateToSensor } = useNavigationStore() + const objectTitle = currentObject?.title + + const [dashboardAlerts, setDashboardAlerts] = useState([]) + const [rawChartData, setRawChartData] = useState<{ timestamp: string; value: number }[]>([]) + const [sensorTypes] = useState>([ + { code: '', name: 'Все датчики' }, + { code: 'GA', name: 'Инклинометр' }, + { code: 'PE', name: 'Танзометр' }, + { code: 'GLE', name: 'Гидроуровень' } + ]) + const [selectedSensorType, setSelectedSensorType] = useState('') + const [selectedChartPeriod, setSelectedChartPeriod] = useState('168') + const [selectedTablePeriod, setSelectedTablePeriod] = useState('168') + + useEffect(() => { + const loadDashboard = async () => { + try { + const params = new URLSearchParams() + params.append('time_period', selectedChartPeriod) + + const res = await fetch(`/api/get-dashboard?${params.toString()}`, { cache: 'no-store' }) + if (!res.ok) return + const payload = await res.json() + console.log('[Dashboard] GET /api/get-dashboard', { status: res.status, payload }) + + let tableData = payload?.data?.table_data ?? [] + tableData = Array.isArray(tableData) ? tableData : [] + + if (objectTitle) { + tableData = tableData.filter((a: any) => a.object === objectTitle) + } + + if (selectedSensorType && selectedSensorType !== '') { + tableData = tableData.filter((a: any) => { + return a.detector_type?.toLowerCase() === selectedSensorType.toLowerCase() + }) + } + + setDashboardAlerts(tableData as any[]) + + const cd = Array.isArray(payload?.data?.chart_data) ? payload.data.chart_data : [] + setRawChartData(cd as any[]) + } catch (e) { + console.error('Failed to load dashboard:', e) + } + } + loadDashboard() + }, [objectTitle, selectedChartPeriod, selectedSensorType]) + + // Отдельный эффект для загрузки таблицы по выбранному периоду + useEffect(() => { + const loadTableData = async () => { + try { + const params = new URLSearchParams() + params.append('time_period', selectedTablePeriod) + + const res = await fetch(`/api/get-dashboard?${params.toString()}`, { cache: 'no-store' }) + if (!res.ok) return + const payload = await res.json() + console.log('[Dashboard] GET /api/get-dashboard (table)', { status: res.status, payload }) + + let tableData = payload?.data?.table_data ?? [] + tableData = Array.isArray(tableData) ? tableData : [] + + if (objectTitle) { + tableData = tableData.filter((a: any) => a.object === objectTitle) + } + + if (selectedSensorType && selectedSensorType !== '') { + tableData = tableData.filter((a: any) => { + return a.detector_type?.toLowerCase() === selectedSensorType.toLowerCase() + }) + } + + setDashboardAlerts(tableData as any[]) + } catch (e) { + console.error('Failed to load table data:', e) + } + } + loadTableData() + }, [objectTitle, selectedTablePeriod, selectedSensorType]) + + const handleBackClick = () => { + router.push('/objects') + } + + const filteredAlerts = dashboardAlerts.filter((alert: any) => { + if (selectedSensorType === '') return true + return alert.detector_type?.toLowerCase() === selectedSensorType.toLowerCase() + }) + + // Статусы + const statusCounts = filteredAlerts.reduce((acc: { critical: number; warning: number; normal: number }, a: any) => { + if (a.severity === 'critical') acc.critical++ + else if (a.severity === 'warning') acc.warning++ + else acc.normal++ + return acc + }, { critical: 0, warning: 0, normal: 0 }) + + const handleNavigationClick = () => { + closeMonitoring() + closeFloorNavigation() + closeNotifications() + setCurrentSubmenu(null) + router.push('/navigation') + } + + const handleGoTo3D = async (alert: any, viewType: 'building' | 'floor') => { + // Используем alert.name как идентификатор датчика (например, "GA-11") + const sensorId = alert.serial_number || alert.name + + if (!sensorId) { + console.warn('[Dashboard] Alert missing sensor identifier:', alert) + return + } + + const sensorSerialNumber = await navigateToSensor( + sensorId, + alert.floor || null, + viewType + ) + + if (sensorSerialNumber) { + // Переходим на страницу навигации с параметром focusSensorId + router.push(`/navigation?focusSensorId=${encodeURIComponent(sensorSerialNumber)}`) + } + } + + const handleSensorTypeChange = (sensorType: string) => { + setSelectedSensorType(sensorType) + } + + const handleChartPeriodChange = (period: string) => { + setSelectedChartPeriod(period) + } + + const handleTablePeriodChange = (period: string) => { + setSelectedTablePeriod(period) + } + + // Агрегируем данные графика в зависимости от периода + const chartData = useMemo(() => { + return aggregateChartDataByDays(rawChartData, selectedChartPeriod) + }, [rawChartData, selectedChartPeriod]) + + const interSemiboldStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 600 } + const interRegularStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 400 } + + return ( +
+ +
+ +
+ +
+
+
+ + +
+
+ +
+
+

{objectTitle || 'Объект'}

+ +
+
+ + + + +
+ +
+ + +
+ + + + +
+
+
+ + {/* Карты-графики */} +
+ + + + + + ({ value: d.value, label: d.label }))} /> + +
+
+ + {/* Список детекторов */} +
+
+
+

Тренды

+
+ + + + +
+
+ + {/* Таблица */} +
+
+ + + + + + + + + + + + + {filteredAlerts.map((alert: any) => ( + + + + + + + + + ))} + +
ДетекторСообщениеСерьезностьДатаРешен3D Вид
{alert.name}{alert.message} + + {alert.severity === 'critical' ? 'Критическое' : alert.severity === 'warning' ? 'Предупреждение' : 'Норма'} + + {new Date(alert.created_at).toLocaleString()} + {alert.resolved ? ( + Да + ) : ( + Нет + ) + } + +
+ + +
+
+
+
+ + {/* Статистика */} +
+
+
{filteredAlerts.length}
+
Всего
+
+
+
{statusCounts.normal}
+
Норма
+
+
+
{statusCounts.warning}
+
Предупреждения
+
+
+
{statusCounts.critical}
+
Критические
+
+
+
+
+
+
+
+ ) +} + +export default Dashboard diff --git a/frontend/lib/chartDataAggregator.ts b/frontend/lib/chartDataAggregator.ts index add25fc..4f42d25 100644 --- a/frontend/lib/chartDataAggregator.ts +++ b/frontend/lib/chartDataAggregator.ts @@ -158,3 +158,97 @@ export function aggregateChartDataByDaysAverage( return dailyData } + +/** + * Интерфейс для данных с разделением по типам событий + */ +export interface SeverityChartDataPoint { + timestamp: string + critical: number + warning: number + label?: string +} + +/** + * Агрегирует данные тревог по дням с разделением по severity + * @param alerts - Массив тревог с полями timestamp и severity + * @param timePeriod - Период в часах ('24', '72', '168', '720') + * @returns Агрегированные данные с разделением по critical/warning + */ +export function aggregateAlertsBySeverity( + alerts: Array<{ timestamp?: string; created_at?: string; severity?: string }>, + timePeriod: string +): SeverityChartDataPoint[] { + if (!Array.isArray(alerts) || alerts.length === 0) { + return [] + } + + // Группируем по дням + const dailyMap = new Map() + + alerts.forEach(alert => { + const timestampField = alert.timestamp || alert.created_at + if (!timestampField) return + + const date = new Date(timestampField) + const dateKey = date.toISOString().split('T')[0] + + if (!dailyMap.has(dateKey)) { + dailyMap.set(dateKey, { critical: 0, warning: 0, date }) + } + + const entry = dailyMap.get(dateKey)! + + if (alert.severity === 'critical') { + entry.critical += 1 + } else if (alert.severity === 'warning') { + entry.warning += 1 + } + }) + + // Преобразуем в массив и сортируем по дате + const dailyData = Array.from(dailyMap.entries()) + .sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime()) + .map(([dateKey, data]) => { + const date = new Date(dateKey) + + // Форматируем подпись в зависимости от периода + let label = '' + if (timePeriod === '24') { + // День - показываем время + label = date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit' + }) + } else if (timePeriod === '72') { + // 3 дня - показываем день недели и дату + label = date.toLocaleDateString('ru-RU', { + weekday: 'short', + day: '2-digit', + month: '2-digit' + }) + } else if (timePeriod === '168') { + // Неделя - показываем день недели + label = date.toLocaleDateString('ru-RU', { + weekday: 'short', + day: '2-digit' + }) + } else if (timePeriod === '720') { + // Месяц - показываем дату + label = date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit' + }) + } + + return { + timestamp: date.toISOString(), + critical: data.critical, + warning: data.warning, + label + } + }) + + return dailyData +} diff --git a/frontend/lib/chartDataAggregator — копия.ts b/frontend/lib/chartDataAggregator — копия.ts new file mode 100644 index 0000000..add25fc --- /dev/null +++ b/frontend/lib/chartDataAggregator — копия.ts @@ -0,0 +1,160 @@ +/** + * Утилита для агрегации часовых данных в дневные + */ + +export interface ChartDataPoint { + timestamp: string + value: number + label?: string +} + +/** + * Агрегирует часовые данные в дневные на основе периода + * @param hourlyData - Массив часовых данных + * @param timePeriod - Период в часах ('24', '72', '168', '720') + * @returns Агрегированные дневные данные + */ +export function aggregateChartDataByDays( + hourlyData: ChartDataPoint[], + timePeriod: string +): ChartDataPoint[] { + if (!Array.isArray(hourlyData) || hourlyData.length === 0) { + return [] + } + + // Для периода в 24 часа - возвращаем часовые данные как есть + if (timePeriod === '24') { + return hourlyData.map(d => ({ + ...d, + label: d.label || new Date(d.timestamp).toLocaleTimeString('ru-RU', { + hour: '2-digit', + minute: '2-digit' + }) + })) + } + + // Для остальных периодов - агрегируем по дням + const dailyMap = new Map() + + hourlyData.forEach(point => { + const date = new Date(point.timestamp) + // Получаем дату в формате YYYY-MM-DD + const dateKey = date.toISOString().split('T')[0] + + if (!dailyMap.has(dateKey)) { + dailyMap.set(dateKey, { sum: 0, count: 0, date }) + } + + const entry = dailyMap.get(dateKey)! + entry.sum += point.value + entry.count += 1 + }) + + // Преобразуем в массив и сортируем по дате + const dailyData = Array.from(dailyMap.entries()) + .sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime()) + .map(([dateKey, data]) => { + const date = new Date(dateKey) + + // Форматируем подпись в зависимости от периода + let label = '' + if (timePeriod === '72') { + // 3 дня - показываем день недели и дату + label = date.toLocaleDateString('ru-RU', { + weekday: 'short', + day: '2-digit', + month: '2-digit' + }) + } else if (timePeriod === '168') { + // Неделя - показываем день недели + label = date.toLocaleDateString('ru-RU', { + weekday: 'short', + day: '2-digit' + }) + } else if (timePeriod === '720') { + // Месяц - показываем дату + label = date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit' + }) + } + + return { + timestamp: date.toISOString(), + value: data.sum, + label + } + }) + + return dailyData +} + +/** + * Получает среднее значение за день (для альтернативной агрегации) + */ +export function aggregateChartDataByDaysAverage( + hourlyData: ChartDataPoint[], + timePeriod: string +): ChartDataPoint[] { + if (!Array.isArray(hourlyData) || hourlyData.length === 0) { + return [] + } + + if (timePeriod === '24') { + return hourlyData.map(d => ({ + ...d, + label: d.label || new Date(d.timestamp).toLocaleTimeString('ru-RU', { + hour: '2-digit', + minute: '2-digit' + }) + })) + } + + const dailyMap = new Map() + + hourlyData.forEach(point => { + const date = new Date(point.timestamp) + const dateKey = date.toISOString().split('T')[0] + + if (!dailyMap.has(dateKey)) { + dailyMap.set(dateKey, { sum: 0, count: 0, date }) + } + + const entry = dailyMap.get(dateKey)! + entry.sum += point.value + entry.count += 1 + }) + + const dailyData = Array.from(dailyMap.entries()) + .sort((a, b) => new Date(a[0]).getTime() - new Date(b[0]).getTime()) + .map(([dateKey, data]) => { + const date = new Date(dateKey) + + let label = '' + if (timePeriod === '72') { + label = date.toLocaleDateString('ru-RU', { + weekday: 'short', + day: '2-digit', + month: '2-digit' + }) + } else if (timePeriod === '168') { + label = date.toLocaleDateString('ru-RU', { + weekday: 'short', + day: '2-digit' + }) + } else if (timePeriod === '720') { + label = date.toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit' + }) + } + + return { + timestamp: date.toISOString(), + value: Math.round(data.sum / data.count), // Среднее значение + label + } + }) + + return dailyData +}