180 lines
6.8 KiB
TypeScript
180 lines
6.8 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState } from 'react'
|
||
|
||
interface AlertType {
|
||
id: number
|
||
detector_id: number
|
||
detector_name: string
|
||
type: string
|
||
status: string
|
||
message: string
|
||
timestamp: string
|
||
location: string
|
||
object: string
|
||
acknowledged: boolean
|
||
priority: 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 SensorsProps {
|
||
objectId?: string
|
||
detectorsData: DetectorsDataType
|
||
onAlertClick: (alert: AlertType) => void
|
||
onClose?: () => void
|
||
is3DReady?: boolean
|
||
}
|
||
|
||
const Sensors: React.FC<SensorsProps> = ({ objectId, detectorsData, onAlertClick, onClose, is3DReady = true }) => {
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
|
||
// Получаем все уведомления (как в компоненте Notifications)
|
||
const alerts = React.useMemo(() => {
|
||
const notifications: AlertType[] = [];
|
||
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,
|
||
status: detector.status
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
// Фильтруем по objectId
|
||
const filteredAlerts = objectId
|
||
? notifications.filter(alert => alert.object && alert.object.toString() === objectId.toString())
|
||
: notifications
|
||
|
||
// Дополнительная фильтрация по поиску
|
||
const finalAlerts = filteredAlerts.filter(alert => {
|
||
return searchTerm === '' ||
|
||
(alert.detector_name && alert.detector_name.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||
(alert.message && alert.message.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||
(alert.location && alert.location.toLowerCase().includes(searchTerm.toLowerCase()))
|
||
})
|
||
|
||
// Сортируем по timestamp (новые сверху)
|
||
return finalAlerts.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||
}, [detectorsData, objectId, searchTerm])
|
||
|
||
const getStatusColor = (type: string) => {
|
||
switch (type.toLowerCase()) {
|
||
case 'critical': return 'bg-red-500'
|
||
case 'warning': return 'bg-orange-500'
|
||
case 'info': return 'bg-green-500'
|
||
default: return 'bg-gray-500'
|
||
}
|
||
}
|
||
|
||
|
||
|
||
return (
|
||
<div className="w-full max-w-2xl">
|
||
<div className="bg-[rgb(22,24,36)] rounded-[12px] p-4 space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="text-white text-2xl font-semibold">Сенсоры - Уведомления</h2>
|
||
<button
|
||
onClick={onClose}
|
||
className="text-white hover:text-gray-300 transition-colors"
|
||
>
|
||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex-1 relative">
|
||
<input
|
||
type="text"
|
||
placeholder="Поиск уведомлений..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="w-full bg-[rgb(27,30,40)] text-white placeholder-gray-400 px-4 py-2 rounded-lg border border-gray-600 focus:border-blue-500 focus:outline-none"
|
||
/>
|
||
<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>
|
||
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2">
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
|
||
</svg>
|
||
Фильтр
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
{alerts.length === 0 ? (
|
||
<div className="flex items-center justify-center h-32">
|
||
<p className="text-gray-400">Уведомления не найдены</p>
|
||
</div>
|
||
) : (
|
||
alerts.map(alert => (
|
||
<div
|
||
key={alert.id}
|
||
className="bg-[rgb(53,58,70)] rounded-md p-3 flex items-center justify-between hover:bg-[rgb(63,68,80)] transition-colors"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<div className={`w-3 h-3 rounded-full ${getStatusColor(alert.type)}`}></div>
|
||
<div>
|
||
<div className="text-white text-sm font-medium">{alert.detector_name}</div>
|
||
<div className="text-gray-400 text-xs">{alert.message}</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
if (is3DReady) {
|
||
onAlertClick(alert)
|
||
} else {
|
||
console.warn('[Sensors] 3D model not ready, skipping alert focus')
|
||
}
|
||
}}
|
||
className="w-6 h-6 bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] rounded-full flex items-center justify-center transition-colors relative"
|
||
title={is3DReady ? "Показать оповещение на 3D модели" : "3D модель недоступна"}
|
||
>
|
||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||
{!is3DReady && (
|
||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-amber-500 rounded-full text-[8px] flex items-center justify-center text-black font-bold">!</div>
|
||
)}
|
||
</button>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Sensors |