Files
aerbim-ht-monitor/frontend/components/reports/ReportsList.tsx
2026-02-02 11:00:40 +03:00

292 lines
12 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, useMemo } from 'react';
import * as statusColors from '../../lib/statusColors';
interface NotificationType {
id: number;
type: string;
message: string;
timestamp: string;
acknowledged: boolean;
priority: string;
detector_id: number;
detector_name: string;
location: string;
object: string;
}
interface DetectorType {
detector_id: number
name: string
serial_number: string
object: string
status: string
type: string
detector_type: string
location: string
floor: number
checked: boolean
notifications: Array<{
id: number
type: string
message: string
timestamp: string
acknowledged: boolean
priority: string
}>
}
interface DetectorsDataType {
detectors: Record<string, DetectorType>
}
interface ReportsListProps {
objectId?: string;
detectorsData: DetectorsDataType;
initialSearchTerm?: string;
}
const ReportsList: React.FC<ReportsListProps> = ({ detectorsData, initialSearchTerm = '' }) => {
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
const [statusFilter, setStatusFilter] = useState('all');
const allNotifications = useMemo(() => {
const notifications: NotificationType[] = [];
Object.values(detectorsData.detectors).forEach(detector => {
if (detector.notifications && detector.notifications.length > 0) {
detector.notifications.forEach(notification => {
notifications.push({
...notification,
detector_id: detector.detector_id,
detector_name: detector.name,
location: detector.location,
object: detector.object
});
});
}
});
return notifications;
}, [detectorsData]);
const filteredDetectors = useMemo(() => {
return allNotifications.filter(notification => {
const matchesSearch = searchTerm === '' || notification.detector_id.toString() === searchTerm;
return matchesSearch;
});
}, [allNotifications, searchTerm]);
const getStatusColor = (type: string) => {
switch (type) {
case 'critical':
return statusColors.STATUS_COLOR_CRITICAL;
case 'warning':
return statusColors.STATUS_COLOR_WARNING;
case 'info':
return statusColors.STATUS_COLOR_NORMAL;
default:
return statusColors.STATUS_COLOR_UNKNOWN;
}
};
const interSemiboldStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 600 }
const interRegularStyle = { fontFamily: 'Inter, sans-serif', fontWeight: 400 }
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high':
return statusColors.STATUS_COLOR_CRITICAL;
case 'medium':
return statusColors.STATUS_COLOR_WARNING;
case 'low':
return statusColors.STATUS_COLOR_NORMAL;
default:
return statusColors.STATUS_COLOR_UNKNOWN;
}
};
const getStatusCounts = () => {
const counts = {
total: allNotifications.length,
critical: allNotifications.filter(d => d.type === 'critical').length,
warning: allNotifications.filter(d => d.type === 'warning').length,
info: allNotifications.filter(d => d.type === 'info').length,
acknowledged: allNotifications.filter(d => d.acknowledged).length,
unacknowledged: allNotifications.filter(d => !d.acknowledged).length
};
return counts;
};
const counts = getStatusCounts();
return (
<div className="space-y-6">
{/* Поиск и сортировка*/}
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<button
onClick={() => setStatusFilter('all')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
statusFilter === 'all'
? 'bg-blue-600 text-white'
: 'bg-[#161824] text-gray-300 hover:bg-[#1f2937]'
}`}
>
Все ({allNotifications.length})
</button>
<button
onClick={() => setStatusFilter('critical')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
statusFilter === 'critical'
? 'bg-red-600 text-white'
: 'bg-[#161824] text-gray-300 hover:bg-[#1f2937]'
}`}
>
Критические ({allNotifications.filter(d => d.type === 'critical').length})
</button>
<button
onClick={() => setStatusFilter('warning')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
statusFilter === 'warning'
? 'bg-orange-600 text-white'
: 'bg-[#161824] text-gray-300 hover:bg-[#1f2937]'
}`}
>
Предупреждения ({allNotifications.filter(d => d.type === 'warning').length})
</button>
<button
onClick={() => setStatusFilter('info')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
statusFilter === 'info'
? 'bg-green-600 text-white'
: 'bg-[#161824] text-gray-300 hover:bg-[#1f2937]'
}`}
>
Информация ({allNotifications.filter(d => d.type === 'info').length})
</button>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<input
type="text"
placeholder="Поиск по ID детектора..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-[#161824] text-white placeholder-gray-400 px-4 py-2 rounded-lg border border-gray-600 focus:border-blue-500 focus:outline-none w-64"
/>
<svg className="absolute right-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
{/* Табличка с детекторами*/}
<div className="bg-[#161824] rounded-[20px] p-6">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-700">
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Детектор</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Статус</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Сообщение</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Местоположение</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Приоритет</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Подтверждено</th>
<th style={interSemiboldStyle} className="text-left text-white text-sm py-3">Время</th>
</tr>
</thead>
<tbody>
{filteredDetectors.map((detector) => (
<tr key={detector.id} className="border-b border-gray-700 hover:bg-gray-800/50 transition-colors">
<td style={interRegularStyle} className="py-4 text-sm text-white">
<div>{detector.detector_name}</div>
<div className="text-gray-400">ID: {detector.detector_id}</div>
</td>
<td className="py-4">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: getStatusColor(detector.type) }}
></div>
<span style={interRegularStyle} className="text-sm text-gray-300">
{detector.type === 'critical' ? 'Критический' :
detector.type === 'warning' ? 'Предупреждение' : 'Информация'}
</span>
</div>
</td>
<td style={interRegularStyle} className="py-4 text-sm text-white">
{detector.message}
</td>
<td style={interRegularStyle} className="py-4 text-sm text-white">
{detector.location}
</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: getPriorityColor(detector.priority) }}
>
{detector.priority === 'high' ? 'Высокий' :
detector.priority === 'medium' ? 'Средний' : 'Низкий'}
</span>
</td>
<td className="py-4">
<span style={interRegularStyle} className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs ${
detector.acknowledged
? 'bg-green-600/20 text-green-300 ring-1 ring-green-600/40'
: 'bg-red-600/20 text-red-300 ring-1 ring-red-600/40'
}`}>
{detector.acknowledged ? 'Да' : 'Нет'}
</span>
</td>
<td style={interRegularStyle} className="py-4 text-sm text-gray-300">
{new Date(detector.timestamp).toLocaleString('ru-RU')}
</td>
</tr>
))}
{filteredDetectors.length === 0 && (
<tr>
<td colSpan={7} className="py-8 text-center text-gray-400">
Детекторы не найдены
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<div className="mt-6 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div className="bg-[#161824] p-4 rounded-lg">
<div className="text-2xl font-bold text-white">{counts.total}</div>
<div className="text-sm text-gray-400">Всего</div>
</div>
<div className="bg-[#161824] p-4 rounded-lg">
<div className="text-2xl font-bold text-red-400">{counts.critical}</div>
<div className="text-sm text-gray-400">Критические</div>
</div>
<div className="bg-[#161824] p-4 rounded-lg">
<div className="text-2xl font-bold text-orange-400">{counts.warning}</div>
<div className="text-sm text-gray-400">Предупреждения</div>
</div>
<div className="bg-[#161824] p-4 rounded-lg">
<div className="text-2xl font-bold text-green-400">{counts.info}</div>
<div className="text-sm text-gray-400">Информация</div>
</div>
<div className="bg-[#161824] p-4 rounded-lg">
<div className="text-2xl font-bold text-blue-400">{counts.acknowledged}</div>
<div className="text-sm text-gray-400">Подтверждено</div>
</div>
<div className="bg-[#161824] p-4 rounded-lg">
<div className="text-2xl font-bold text-yellow-400">{counts.unacknowledged}</div>
<div className="text-sm text-gray-400">Не подтверждено</div>
</div>
</div>
</div>
);
};
export default ReportsList;