Added list of detectors and sensors sub-menus

This commit is contained in:
iv_vuytsik
2025-11-19 09:48:17 +03:00
parent 46045f0b0a
commit eba7585a5b
9 changed files with 814 additions and 19 deletions

View File

@@ -0,0 +1,180 @@
'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