Files
aerbim-ht-monitor/frontend/components/reports/ReportsList.tsx
2025-11-11 10:07:38 +03:00

290 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';
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;
}
const ReportsList: React.FC<ReportsListProps> = ({ detectorsData }) => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [priorityFilter] = useState('all');
const [acknowledgedFilter] = 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 = notification.detector_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
notification.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
notification.message.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || notification.type === statusFilter;
const matchesPriority = priorityFilter === 'all' || notification.priority === priorityFilter;
const matchesAcknowledged = acknowledgedFilter === 'all' ||
(acknowledgedFilter === 'acknowledged' && notification.acknowledged) ||
(acknowledgedFilter === 'unacknowledged' && !notification.acknowledged);
return matchesSearch && matchesStatus && matchesPriority && matchesAcknowledged;
});
}, [allNotifications, searchTerm, statusFilter, priorityFilter, acknowledgedFilter]);
const getStatusColor = (type: string) => {
switch (type) {
case 'critical': return '#b3261e';
case 'warning': return '#fd7c22';
case 'info': return '#00ff00';
default: return '#666';
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return '#b3261e';
case 'medium': return '#fd7c22';
case 'low': return '#00ff00';
default: return '#666';
}
};
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="Поиск детекторов..."
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 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>
{filteredDetectors.map((detector) => (
<tr key={detector.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">{detector.detector_name}</div>
<div className="text-sm 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 className="text-sm text-gray-300">
{detector.type === 'critical' ? 'Критический' :
detector.type === 'warning' ? 'Предупреждение' : 'Информация'}
</span>
</div>
</td>
<td className="py-4">
<div className="text-sm text-white">{detector.message}</div>
</td>
<td className="py-4">
<div className="text-sm text-white">{detector.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: getPriorityColor(detector.priority) }}
>
{detector.priority === 'high' ? 'Высокий' :
detector.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 ${
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 className="py-4">
<div className="text-sm text-gray-300">
{new Date(detector.timestamp).toLocaleString('ru-RU')}
</div>
</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;