Added list of detectors and sensors sub-menus
This commit is contained in:
@@ -22,12 +22,14 @@ class AlertSerializer(serializers.ModelSerializer):
|
|||||||
def get_object(self, obj) -> Optional[str]:
|
def get_object(self, obj) -> Optional[str]:
|
||||||
zone = obj.sensor.zones.first()
|
zone = obj.sensor.zones.first()
|
||||||
return zone.object.title if zone else None
|
return zone.object.title if zone else None
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_metric_value(self, obj) -> str:
|
def get_metric_value(self, obj) -> str:
|
||||||
if obj.metric.value is not None:
|
if obj.metric.value is not None:
|
||||||
unit = obj.sensor.signal_format.unit if obj.sensor.signal_format else ''
|
unit = obj.sensor.signal_format.unit if obj.sensor.signal_format else ''
|
||||||
return f"{obj.metric.value} {unit}".strip()
|
return f"{obj.metric.value} {unit}".strip()
|
||||||
return obj.metric.raw_value
|
return obj.metric.raw_value
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.STR)
|
@extend_schema_field(OpenApiTypes.STR)
|
||||||
def get_detector_type(self, obj) -> str:
|
def get_detector_type(self, obj) -> str:
|
||||||
sensor_type = getattr(obj, 'sensor_type', None)
|
sensor_type = getattr(obj, 'sensor_type', None)
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import useNavigationStore from '../../store/navigationStore'
|
|||||||
import Monitoring from '../../../components/navigation/Monitoring'
|
import Monitoring from '../../../components/navigation/Monitoring'
|
||||||
import FloorNavigation from '../../../components/navigation/FloorNavigation'
|
import FloorNavigation from '../../../components/navigation/FloorNavigation'
|
||||||
import DetectorMenu from '../../../components/navigation/DetectorMenu'
|
import DetectorMenu from '../../../components/navigation/DetectorMenu'
|
||||||
|
import ListOfDetectors from '../../../components/navigation/ListOfDetectors'
|
||||||
|
import Sensors from '../../../components/navigation/Sensors'
|
||||||
|
import AlertMenu from '../../../components/navigation/AlertMenu'
|
||||||
import Notifications from '../../../components/notifications/Notifications'
|
import Notifications from '../../../components/notifications/Notifications'
|
||||||
import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo'
|
import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
@@ -56,6 +59,20 @@ interface NotificationType {
|
|||||||
priority: string
|
priority: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
const NavigationPage: React.FC = () => {
|
const NavigationPage: React.FC = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
@@ -65,17 +82,25 @@ const NavigationPage: React.FC = () => {
|
|||||||
showMonitoring,
|
showMonitoring,
|
||||||
showFloorNavigation,
|
showFloorNavigation,
|
||||||
showNotifications,
|
showNotifications,
|
||||||
|
showListOfDetectors,
|
||||||
|
showSensors,
|
||||||
selectedDetector,
|
selectedDetector,
|
||||||
showDetectorMenu,
|
showDetectorMenu,
|
||||||
selectedNotification,
|
selectedNotification,
|
||||||
showNotificationDetectorInfo,
|
showNotificationDetectorInfo,
|
||||||
|
selectedAlert,
|
||||||
|
showAlertMenu,
|
||||||
closeMonitoring,
|
closeMonitoring,
|
||||||
closeFloorNavigation,
|
closeFloorNavigation,
|
||||||
closeNotifications,
|
closeNotifications,
|
||||||
|
closeListOfDetectors,
|
||||||
|
closeSensors,
|
||||||
setSelectedDetector,
|
setSelectedDetector,
|
||||||
setShowDetectorMenu,
|
setShowDetectorMenu,
|
||||||
setSelectedNotification,
|
setSelectedNotification,
|
||||||
setShowNotificationDetectorInfo
|
setShowNotificationDetectorInfo,
|
||||||
|
setSelectedAlert,
|
||||||
|
setShowAlertMenu
|
||||||
} = useNavigationStore()
|
} = useNavigationStore()
|
||||||
|
|
||||||
const [detectorsData, setDetectorsData] = useState<{ detectors: Record<string, DetectorType> }>({ detectors: {} })
|
const [detectorsData, setDetectorsData] = useState<{ detectors: Record<string, DetectorType> }>({ detectors: {} })
|
||||||
@@ -181,6 +206,32 @@ const NavigationPage: React.FC = () => {
|
|||||||
setSelectedNotification(null)
|
setSelectedNotification(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeAlertMenu = () => {
|
||||||
|
setShowAlertMenu(false)
|
||||||
|
setSelectedAlert(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAlertClick = (alert: AlertType) => {
|
||||||
|
console.log('[NavigationPage] Alert clicked, focusing on detector in 3D scene:', alert)
|
||||||
|
|
||||||
|
const detector = Object.values(detectorsData.detectors).find(
|
||||||
|
d => d.detector_id === alert.detector_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (detector) {
|
||||||
|
console.log('[NavigationPage] Found detector for alert:', detector)
|
||||||
|
|
||||||
|
setSelectedAlert(alert)
|
||||||
|
setShowAlertMenu(true)
|
||||||
|
|
||||||
|
setSelectedDetector(detector)
|
||||||
|
|
||||||
|
console.log('[NavigationPage] Showing AlertMenu for alert:', alert.detector_name)
|
||||||
|
} else {
|
||||||
|
console.warn('[NavigationPage] Could not find detector for alert:', alert.detector_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
const s = (status || '').toLowerCase()
|
const s = (status || '').toLowerCase()
|
||||||
switch (s) {
|
switch (s) {
|
||||||
@@ -253,6 +304,40 @@ const NavigationPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showListOfDetectors && (
|
||||||
|
<div className="absolute left-0 top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-20 w-[500px]">
|
||||||
|
<div className="h-full overflow-auto p-4">
|
||||||
|
<ListOfDetectors
|
||||||
|
objectId={objectId || undefined}
|
||||||
|
detectorsData={detectorsData}
|
||||||
|
onDetectorMenuClick={handleDetectorMenuClick}
|
||||||
|
onClose={closeListOfDetectors}
|
||||||
|
is3DReady={isModelReady && !modelError}
|
||||||
|
/>
|
||||||
|
{detectorsError && (
|
||||||
|
<div className="mt-2 text-sm text-red-400">{detectorsError}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSensors && (
|
||||||
|
<div className="absolute left-0 top-[73px] bottom-0 bg-[#161824] border-r border-gray-700 z-20 w-[500px]">
|
||||||
|
<div className="h-full overflow-auto p-4">
|
||||||
|
<Sensors
|
||||||
|
objectId={objectId || undefined}
|
||||||
|
detectorsData={detectorsData}
|
||||||
|
onAlertClick={handleAlertClick}
|
||||||
|
onClose={closeSensors}
|
||||||
|
is3DReady={isModelReady && !modelError}
|
||||||
|
/>
|
||||||
|
{detectorsError && (
|
||||||
|
<div className="mt-2 text-sm text-red-400">{detectorsError}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showNotifications && showNotificationDetectorInfo && selectedNotification && (() => {
|
{showNotifications && showNotificationDetectorInfo && selectedNotification && (() => {
|
||||||
const detectorData = Object.values(detectorsData.detectors).find(
|
const detectorData = Object.values(detectorsData.detectors).find(
|
||||||
detector => detector.detector_id === selectedNotification.detector_id
|
detector => detector.detector_id === selectedNotification.detector_id
|
||||||
@@ -334,9 +419,19 @@ const NavigationPage: React.FC = () => {
|
|||||||
modelPath={selectedModelPath}
|
modelPath={selectedModelPath}
|
||||||
onModelLoaded={handleModelLoaded}
|
onModelLoaded={handleModelLoaded}
|
||||||
onError={handleModelError}
|
onError={handleModelError}
|
||||||
focusSensorId={selectedDetector?.serial_number ?? null}
|
focusSensorId={selectedDetector?.serial_number ?? selectedAlert?.detector_id?.toString() ?? null}
|
||||||
renderOverlay={({ anchor }) => (
|
renderOverlay={({ anchor }) => (
|
||||||
selectedDetector && showDetectorMenu && anchor ? (
|
<>
|
||||||
|
{selectedAlert && showAlertMenu && anchor ? (
|
||||||
|
<AlertMenu
|
||||||
|
alert={selectedAlert}
|
||||||
|
isOpen={true}
|
||||||
|
onClose={closeAlertMenu}
|
||||||
|
getStatusText={getStatusText}
|
||||||
|
compact={true}
|
||||||
|
anchor={anchor}
|
||||||
|
/>
|
||||||
|
) : selectedDetector && showDetectorMenu && anchor ? (
|
||||||
<DetectorMenu
|
<DetectorMenu
|
||||||
detector={selectedDetector}
|
detector={selectedDetector}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
@@ -345,7 +440,8 @@ const NavigationPage: React.FC = () => {
|
|||||||
compact={true}
|
compact={true}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,6 +36,20 @@ interface NotificationType {
|
|||||||
priority: string
|
priority: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
export interface NavigationStore {
|
export interface NavigationStore {
|
||||||
currentObject: { id: string | undefined; title: string | undefined }
|
currentObject: { id: string | undefined; title: string | undefined }
|
||||||
navigationHistory: string[]
|
navigationHistory: string[]
|
||||||
@@ -44,11 +58,15 @@ export interface NavigationStore {
|
|||||||
showMonitoring: boolean
|
showMonitoring: boolean
|
||||||
showFloorNavigation: boolean
|
showFloorNavigation: boolean
|
||||||
showNotifications: boolean
|
showNotifications: boolean
|
||||||
|
showListOfDetectors: boolean
|
||||||
|
showSensors: boolean
|
||||||
|
|
||||||
selectedDetector: DetectorType | null
|
selectedDetector: DetectorType | null
|
||||||
showDetectorMenu: boolean
|
showDetectorMenu: boolean
|
||||||
selectedNotification: NotificationType | null
|
selectedNotification: NotificationType | null
|
||||||
showNotificationDetectorInfo: boolean
|
showNotificationDetectorInfo: boolean
|
||||||
|
selectedAlert: AlertType | null
|
||||||
|
showAlertMenu: boolean
|
||||||
|
|
||||||
setCurrentObject: (id: string | undefined, title: string | undefined) => void
|
setCurrentObject: (id: string | undefined, title: string | undefined) => void
|
||||||
clearCurrentObject: () => void
|
clearCurrentObject: () => void
|
||||||
@@ -64,11 +82,20 @@ export interface NavigationStore {
|
|||||||
closeFloorNavigation: () => void
|
closeFloorNavigation: () => void
|
||||||
openNotifications: () => void
|
openNotifications: () => void
|
||||||
closeNotifications: () => void
|
closeNotifications: () => void
|
||||||
|
openListOfDetectors: () => void
|
||||||
|
closeListOfDetectors: () => void
|
||||||
|
openSensors: () => void
|
||||||
|
closeSensors: () => void
|
||||||
|
|
||||||
|
// Close all menus and submenus
|
||||||
|
closeAllMenus: () => void
|
||||||
|
|
||||||
setSelectedDetector: (detector: DetectorType | null) => void
|
setSelectedDetector: (detector: DetectorType | null) => void
|
||||||
setShowDetectorMenu: (show: boolean) => void
|
setShowDetectorMenu: (show: boolean) => void
|
||||||
setSelectedNotification: (notification: NotificationType | null) => void
|
setSelectedNotification: (notification: NotificationType | null) => void
|
||||||
setShowNotificationDetectorInfo: (show: boolean) => void
|
setShowNotificationDetectorInfo: (show: boolean) => void
|
||||||
|
setSelectedAlert: (alert: AlertType | null) => void
|
||||||
|
setShowAlertMenu: (show: boolean) => void
|
||||||
|
|
||||||
isOnNavigationPage: () => boolean
|
isOnNavigationPage: () => boolean
|
||||||
getCurrentRoute: () => string | null
|
getCurrentRoute: () => string | null
|
||||||
@@ -88,11 +115,15 @@ const useNavigationStore = create<NavigationStore>()(
|
|||||||
showMonitoring: false,
|
showMonitoring: false,
|
||||||
showFloorNavigation: false,
|
showFloorNavigation: false,
|
||||||
showNotifications: false,
|
showNotifications: false,
|
||||||
|
showListOfDetectors: false,
|
||||||
|
showSensors: false,
|
||||||
|
|
||||||
selectedDetector: null,
|
selectedDetector: null,
|
||||||
showDetectorMenu: false,
|
showDetectorMenu: false,
|
||||||
selectedNotification: null,
|
selectedNotification: null,
|
||||||
showNotificationDetectorInfo: false,
|
showNotificationDetectorInfo: false,
|
||||||
|
selectedAlert: null,
|
||||||
|
showAlertMenu: false,
|
||||||
|
|
||||||
setCurrentObject: (id: string | undefined, title: string | undefined) =>
|
setCurrentObject: (id: string | undefined, title: string | undefined) =>
|
||||||
set({ currentObject: { id, title } }),
|
set({ currentObject: { id, title } }),
|
||||||
@@ -131,6 +162,7 @@ const useNavigationStore = create<NavigationStore>()(
|
|||||||
showMonitoring: true,
|
showMonitoring: true,
|
||||||
showFloorNavigation: false,
|
showFloorNavigation: false,
|
||||||
showNotifications: false,
|
showNotifications: false,
|
||||||
|
showListOfDetectors: false,
|
||||||
currentSubmenu: 'monitoring',
|
currentSubmenu: 'monitoring',
|
||||||
showDetectorMenu: false,
|
showDetectorMenu: false,
|
||||||
selectedDetector: null,
|
selectedDetector: null,
|
||||||
@@ -147,6 +179,7 @@ const useNavigationStore = create<NavigationStore>()(
|
|||||||
showFloorNavigation: true,
|
showFloorNavigation: true,
|
||||||
showMonitoring: false,
|
showMonitoring: false,
|
||||||
showNotifications: false,
|
showNotifications: false,
|
||||||
|
showListOfDetectors: false,
|
||||||
currentSubmenu: 'floors',
|
currentSubmenu: 'floors',
|
||||||
showNotificationDetectorInfo: false,
|
showNotificationDetectorInfo: false,
|
||||||
selectedNotification: null
|
selectedNotification: null
|
||||||
@@ -163,6 +196,7 @@ const useNavigationStore = create<NavigationStore>()(
|
|||||||
showNotifications: true,
|
showNotifications: true,
|
||||||
showMonitoring: false,
|
showMonitoring: false,
|
||||||
showFloorNavigation: false,
|
showFloorNavigation: false,
|
||||||
|
showListOfDetectors: false,
|
||||||
currentSubmenu: 'notifications',
|
currentSubmenu: 'notifications',
|
||||||
showDetectorMenu: false,
|
showDetectorMenu: false,
|
||||||
selectedDetector: null
|
selectedDetector: null
|
||||||
@@ -175,10 +209,67 @@ const useNavigationStore = create<NavigationStore>()(
|
|||||||
currentSubmenu: null
|
currentSubmenu: null
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
openListOfDetectors: () => set({
|
||||||
|
showListOfDetectors: true,
|
||||||
|
showMonitoring: false,
|
||||||
|
showFloorNavigation: false,
|
||||||
|
showNotifications: false,
|
||||||
|
currentSubmenu: 'detectors',
|
||||||
|
showDetectorMenu: false,
|
||||||
|
selectedDetector: null,
|
||||||
|
showNotificationDetectorInfo: false,
|
||||||
|
selectedNotification: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
closeListOfDetectors: () => set({
|
||||||
|
showListOfDetectors: false,
|
||||||
|
showDetectorMenu: false,
|
||||||
|
selectedDetector: null,
|
||||||
|
currentSubmenu: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
openSensors: () => set({
|
||||||
|
showSensors: true,
|
||||||
|
showMonitoring: false,
|
||||||
|
showFloorNavigation: false,
|
||||||
|
showNotifications: false,
|
||||||
|
showListOfDetectors: false,
|
||||||
|
currentSubmenu: 'sensors',
|
||||||
|
showDetectorMenu: false,
|
||||||
|
selectedDetector: null,
|
||||||
|
showNotificationDetectorInfo: false,
|
||||||
|
selectedNotification: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
closeSensors: () => set({
|
||||||
|
showSensors: false,
|
||||||
|
showDetectorMenu: false,
|
||||||
|
selectedDetector: null,
|
||||||
|
currentSubmenu: null
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Close all menus and submenus
|
||||||
|
closeAllMenus: () => set({
|
||||||
|
showMonitoring: false,
|
||||||
|
showFloorNavigation: false,
|
||||||
|
showNotifications: false,
|
||||||
|
showListOfDetectors: false,
|
||||||
|
showSensors: false,
|
||||||
|
showDetectorMenu: false,
|
||||||
|
selectedDetector: null,
|
||||||
|
showNotificationDetectorInfo: false,
|
||||||
|
selectedNotification: null,
|
||||||
|
showAlertMenu: false,
|
||||||
|
selectedAlert: null,
|
||||||
|
currentSubmenu: null
|
||||||
|
}),
|
||||||
|
|
||||||
setSelectedDetector: (detector: DetectorType | null) => set({ selectedDetector: detector }),
|
setSelectedDetector: (detector: DetectorType | null) => set({ selectedDetector: detector }),
|
||||||
setShowDetectorMenu: (show: boolean) => set({ showDetectorMenu: show }),
|
setShowDetectorMenu: (show: boolean) => set({ showDetectorMenu: show }),
|
||||||
setSelectedNotification: (notification: NotificationType | null) => set({ selectedNotification: notification }),
|
setSelectedNotification: (notification: NotificationType | null) => set({ selectedNotification: notification }),
|
||||||
setShowNotificationDetectorInfo: (show: boolean) => set({ showNotificationDetectorInfo: show }),
|
setShowNotificationDetectorInfo: (show: boolean) => set({ showNotificationDetectorInfo: show }),
|
||||||
|
setSelectedAlert: (alert: AlertType | null) => set({ selectedAlert: alert }),
|
||||||
|
setShowAlertMenu: (show: boolean) => set({ showAlertMenu: show }),
|
||||||
|
|
||||||
isOnNavigationPage: () => {
|
isOnNavigationPage: () => {
|
||||||
const { navigationHistory } = get()
|
const { navigationHistory } = get()
|
||||||
@@ -192,10 +283,12 @@ const useNavigationStore = create<NavigationStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
getActiveSidebarItem: () => {
|
getActiveSidebarItem: () => {
|
||||||
const { showMonitoring, showFloorNavigation, showNotifications } = get()
|
const { showMonitoring, showFloorNavigation, showNotifications, showListOfDetectors, showSensors } = get()
|
||||||
if (showMonitoring) return 3 // Зоны Мониторинга
|
if (showMonitoring) return 3 // Зоны Мониторинга
|
||||||
if (showFloorNavigation) return 4 // Навигация по этажам
|
if (showFloorNavigation) return 4 // Навигация по этажам
|
||||||
if (showNotifications) return 5 // Уведомления
|
if (showNotifications) return 5 // Уведомления
|
||||||
|
if (showListOfDetectors) return 7 // Список датчиков
|
||||||
|
if (showSensors) return 8 // Сенсоры
|
||||||
return 2 // Навигация (базовая)
|
return 2 // Навигация (базовая)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
|||||||
const meta: any = (m as any)?.metadata
|
const meta: any = (m as any)?.metadata
|
||||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
||||||
|
|
||||||
const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number
|
const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number ?? extras?.detector_id
|
||||||
if (sid != null) {
|
if (sid != null) {
|
||||||
return String(sid).trim() === sensorId
|
return String(sid).trim() === sensorId
|
||||||
}
|
}
|
||||||
|
|||||||
199
frontend/components/navigation/AlertMenu.tsx
Normal file
199
frontend/components/navigation/AlertMenu.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React 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 AlertMenuProps {
|
||||||
|
alert: AlertType
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
getStatusText: (status: string) => string
|
||||||
|
compact?: boolean
|
||||||
|
anchor?: { left: number; top: number } | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatusText, compact = false, anchor = null }) => {
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPriorityColor = (priority: string) => {
|
||||||
|
switch (priority.toLowerCase()) {
|
||||||
|
case 'high': return 'text-red-400'
|
||||||
|
case 'medium': return 'text-orange-400'
|
||||||
|
case 'low': return 'text-green-400'
|
||||||
|
default: return 'text-gray-400'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPriorityText = (priority: string) => {
|
||||||
|
switch (priority.toLowerCase()) {
|
||||||
|
case 'high': return 'Высокий'
|
||||||
|
case 'medium': return 'Средний'
|
||||||
|
case 'low': return 'Низкий'
|
||||||
|
default: return priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compact && anchor) {
|
||||||
|
return (
|
||||||
|
<div className="absolute z-40" style={{ left: anchor.left, top: anchor.top }}>
|
||||||
|
<div className="rounded-[10px] bg-black/80 text-white text-xs px-3 py-2 shadow-xl min-w-[240px] max-w-[300px]">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-semibold truncate">Датч.{alert.detector_name}</div>
|
||||||
|
<div className="opacity-80">{getStatusText(alert.status)}</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="text-gray-300 hover:text-white transition-colors">
|
||||||
|
<svg className="w-4 h-4" 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="mt-2 space-y-1">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-[rgb(113,113,122)] text-[11px]">Приоритет</div>
|
||||||
|
<div className={`text-xs font-medium ${getPriorityColor(alert.priority)}`}>
|
||||||
|
{getPriorityText(alert.priority)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[rgb(113,113,122)] text-[11px]">Время</div>
|
||||||
|
<div className="text-white text-xs truncate">{formatDate(alert.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-[rgb(113,113,122)] text-[11px]">Сообщение</div>
|
||||||
|
<div className="text-white text-xs">{alert.message}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-[rgb(113,113,122)] text-[11px]">Местоположение</div>
|
||||||
|
<div className="text-white text-xs">{alert.location}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||||
|
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-2 py-1 rounded-[8px] text-xs font-medium transition-colors flex items-center gap-1">
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
Отчет
|
||||||
|
</button>
|
||||||
|
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-2 py-1 rounded-[8px] text-xs font-medium transition-colors flex items-center gap-1">
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
История
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute left-[500px] top-0 bg-[#161824] border-r border-gray-700 z-30 w-[454px]" style={{height: 'calc(100% - 73px)', top: '73px'}}>
|
||||||
|
<div className="h-full overflow-auto p-5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-white text-lg font-medium">
|
||||||
|
Датч.{alert.detector_name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-3 py-2 rounded-[10px] text-sm font-medium 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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
Отчет
|
||||||
|
</button>
|
||||||
|
<button className="bg-[rgb(27,29,41)] hover:bg-[rgb(37,39,51)] text-white px-3 py-2 rounded-[10px] text-sm font-medium 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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
История
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Табличка с информацией об алерте */}
|
||||||
|
<div className="space-y-0 border border-[rgb(30,31,36)] rounded-lg overflow-hidden">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1 p-4 border-r border-[rgb(30,31,36)]">
|
||||||
|
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Маркировка по проекту</div>
|
||||||
|
<div className="text-white text-sm">{alert.detector_name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Приоритет</div>
|
||||||
|
<div className={`text-sm font-medium ${getPriorityColor(alert.priority)}`}>
|
||||||
|
{getPriorityText(alert.priority)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex border-t border-[rgb(30,31,36)]">
|
||||||
|
<div className="flex-1 p-4 border-r border-[rgb(30,31,36)]">
|
||||||
|
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Местоположение</div>
|
||||||
|
<div className="text-white text-sm">{alert.location}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Статус</div>
|
||||||
|
<div className="text-white text-sm">{getStatusText(alert.status)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex border-t border-[rgb(30,31,36)]">
|
||||||
|
<div className="flex-1 p-4 border-r border-[rgb(30,31,36)]">
|
||||||
|
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Время события</div>
|
||||||
|
<div className="text-white text-sm">{formatDate(alert.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Тип алерта</div>
|
||||||
|
<div className="text-white text-sm">{alert.type}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-[rgb(30,31,36)] p-4">
|
||||||
|
<div className="text-[rgb(113,113,122)] text-sm font-medium mb-1">Сообщение</div>
|
||||||
|
<div className="text-white text-sm">{alert.message}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AlertMenu
|
||||||
171
frontend/components/navigation/ListOfDetectors.tsx
Normal file
171
frontend/components/navigation/ListOfDetectors.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
interface DetectorsDataType {
|
||||||
|
detectors: Record<string, DetectorType>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListOfDetectorsProps {
|
||||||
|
objectId?: string
|
||||||
|
detectorsData: DetectorsDataType
|
||||||
|
onDetectorMenuClick: (detector: DetectorType) => void
|
||||||
|
onClose?: () => void
|
||||||
|
is3DReady?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DetectorType {
|
||||||
|
detector_id: number
|
||||||
|
name: string
|
||||||
|
serial_number: string
|
||||||
|
object: string
|
||||||
|
status: string
|
||||||
|
checked: boolean
|
||||||
|
type: string
|
||||||
|
detector_type: string
|
||||||
|
location: string
|
||||||
|
floor: number
|
||||||
|
notifications: Array<{
|
||||||
|
id: number
|
||||||
|
type: string
|
||||||
|
message: string
|
||||||
|
timestamp: string
|
||||||
|
acknowledged: boolean
|
||||||
|
priority: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListOfDetectors: React.FC<ListOfDetectorsProps> = ({ objectId, detectorsData, onDetectorMenuClick, onClose, is3DReady = true }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
|
||||||
|
// конвертация детекторов в array и фильтруем по objectId и тексту запроса
|
||||||
|
const detectorsArray = Object.values(detectorsData.detectors) as DetectorType[]
|
||||||
|
let filteredDetectors = objectId
|
||||||
|
? detectorsArray.filter(detector => detector.object === objectId)
|
||||||
|
: detectorsArray
|
||||||
|
|
||||||
|
// Фильтр-поиск
|
||||||
|
if (searchTerm) {
|
||||||
|
filteredDetectors = filteredDetectors.filter(detector =>
|
||||||
|
detector.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
detector.location.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
detector.serial_number.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортировка детекторов по имени
|
||||||
|
const sortedDetectors = filteredDetectors.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case '#b3261e': return 'bg-red-500'
|
||||||
|
case '#fd7c22': return 'bg-orange-500'
|
||||||
|
case '#00ff00': return 'bg-green-500'
|
||||||
|
default: return 'bg-gray-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case '#b3261e': return 'Критический'
|
||||||
|
case '#fd7c22': return 'Предупреждение'
|
||||||
|
case '#00ff00': return 'Норма'
|
||||||
|
default: return 'Неизвестно'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDetectorMenuClick = (detector: DetectorType) => {
|
||||||
|
// Проверяем валидность данных детектора перед передачей
|
||||||
|
if (!detector || !detector.detector_id || !detector.serial_number) {
|
||||||
|
console.warn('[ListOfDetectors] Invalid detector data, skipping menu click:', detector)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onDetectorMenuClick(detector)
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
{onClose && (
|
||||||
|
<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>
|
||||||
|
<div className="text-gray-400 text-sm">
|
||||||
|
{sortedDetectors.length} детекторов
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 h-full overflow-y-auto">
|
||||||
|
{sortedDetectors.length === 0 ? (
|
||||||
|
<div className="text-gray-400 text-center py-8">
|
||||||
|
Детекторы не найдены
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
sortedDetectors.map(detector => (
|
||||||
|
<div
|
||||||
|
key={detector.detector_id}
|
||||||
|
className="bg-[rgb(53,58,70)] rounded-md p-3 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-white text-sm font-medium">{detector.name}</div>
|
||||||
|
<div className="text-gray-400 text-xs">{detector.location}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${getStatusColor(detector.status)}`}></div>
|
||||||
|
<span className="text-xs text-gray-300">{getStatusText(detector.status)}</span>
|
||||||
|
{detector.checked && (
|
||||||
|
<svg className="w-4 h-4 text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (is3DReady) {
|
||||||
|
handleDetectorMenuClick(detector)
|
||||||
|
} else {
|
||||||
|
console.warn('[ListOfDetectors] 3D model not ready, skipping detector 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>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListOfDetectors
|
||||||
@@ -8,7 +8,6 @@ interface MonitoringProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
const Monitoring: React.FC<MonitoringProps> = ({ onClose, onSelectModel }) => {
|
||||||
const [objectImageError, setObjectImageError] = useState(false);
|
|
||||||
const [models, setModels] = useState<{ title: string; path: string }[]>([]);
|
const [models, setModels] = useState<{ title: string; path: string }[]>([]);
|
||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|||||||
180
frontend/components/navigation/Sensors.tsx
Normal file
180
frontend/components/navigation/Sensors.tsx
Normal 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
|
||||||
@@ -185,12 +185,19 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
openMonitoring,
|
openMonitoring,
|
||||||
openFloorNavigation,
|
openFloorNavigation,
|
||||||
openNotifications,
|
openNotifications,
|
||||||
|
openListOfDetectors,
|
||||||
|
openSensors,
|
||||||
closeMonitoring,
|
closeMonitoring,
|
||||||
closeFloorNavigation,
|
closeFloorNavigation,
|
||||||
closeNotifications,
|
closeNotifications,
|
||||||
|
closeListOfDetectors,
|
||||||
|
closeSensors,
|
||||||
|
closeAllMenus,
|
||||||
showMonitoring,
|
showMonitoring,
|
||||||
showFloorNavigation,
|
showFloorNavigation,
|
||||||
showNotifications
|
showNotifications,
|
||||||
|
showListOfDetectors,
|
||||||
|
showSensors
|
||||||
} = useNavigationStore()
|
} = useNavigationStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -222,10 +229,14 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
case 3: // Monitoring
|
case 3: // Monitoring
|
||||||
if (pathname !== '/navigation') {
|
if (pathname !== '/navigation') {
|
||||||
router.push('/navigation')
|
router.push('/navigation')
|
||||||
setTimeout(() => openMonitoring(), 100)
|
setTimeout(() => {
|
||||||
|
closeAllMenus()
|
||||||
|
openMonitoring()
|
||||||
|
}, 100)
|
||||||
} else if (showMonitoring) {
|
} else if (showMonitoring) {
|
||||||
closeMonitoring()
|
closeMonitoring()
|
||||||
} else {
|
} else {
|
||||||
|
closeAllMenus()
|
||||||
openMonitoring()
|
openMonitoring()
|
||||||
}
|
}
|
||||||
handled = true
|
handled = true
|
||||||
@@ -233,10 +244,14 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
case 4: // Floor Navigation
|
case 4: // Floor Navigation
|
||||||
if (pathname !== '/navigation') {
|
if (pathname !== '/navigation') {
|
||||||
router.push('/navigation')
|
router.push('/navigation')
|
||||||
setTimeout(() => openFloorNavigation(), 100)
|
setTimeout(() => {
|
||||||
|
closeAllMenus()
|
||||||
|
openFloorNavigation()
|
||||||
|
}, 100)
|
||||||
} else if (showFloorNavigation) {
|
} else if (showFloorNavigation) {
|
||||||
closeFloorNavigation()
|
closeFloorNavigation()
|
||||||
} else {
|
} else {
|
||||||
|
closeAllMenus()
|
||||||
openFloorNavigation()
|
openFloorNavigation()
|
||||||
}
|
}
|
||||||
handled = true
|
handled = true
|
||||||
@@ -244,14 +259,48 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
case 5: // Notifications
|
case 5: // Notifications
|
||||||
if (pathname !== '/navigation') {
|
if (pathname !== '/navigation') {
|
||||||
router.push('/navigation')
|
router.push('/navigation')
|
||||||
setTimeout(() => openNotifications(), 100)
|
setTimeout(() => {
|
||||||
|
closeAllMenus()
|
||||||
|
openNotifications()
|
||||||
|
}, 100)
|
||||||
} else if (showNotifications) {
|
} else if (showNotifications) {
|
||||||
closeNotifications()
|
closeNotifications()
|
||||||
} else {
|
} else {
|
||||||
|
closeAllMenus()
|
||||||
openNotifications()
|
openNotifications()
|
||||||
}
|
}
|
||||||
handled = true
|
handled = true
|
||||||
break
|
break
|
||||||
|
case 6: // Sensors
|
||||||
|
if (pathname !== '/navigation') {
|
||||||
|
router.push('/navigation')
|
||||||
|
setTimeout(() => {
|
||||||
|
closeAllMenus()
|
||||||
|
openSensors()
|
||||||
|
}, 100)
|
||||||
|
} else if (showSensors) {
|
||||||
|
closeSensors()
|
||||||
|
} else {
|
||||||
|
closeAllMenus()
|
||||||
|
openSensors()
|
||||||
|
}
|
||||||
|
handled = true
|
||||||
|
break
|
||||||
|
case 7: // List of Detectors
|
||||||
|
if (pathname !== '/navigation') {
|
||||||
|
router.push('/navigation')
|
||||||
|
setTimeout(() => {
|
||||||
|
closeAllMenus()
|
||||||
|
openListOfDetectors()
|
||||||
|
}, 100)
|
||||||
|
} else if (showListOfDetectors) {
|
||||||
|
closeListOfDetectors()
|
||||||
|
} else {
|
||||||
|
closeAllMenus()
|
||||||
|
openListOfDetectors()
|
||||||
|
}
|
||||||
|
handled = true
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
// Для остального используем routes
|
// Для остального используем routes
|
||||||
if (navigationService) {
|
if (navigationService) {
|
||||||
@@ -330,6 +379,8 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
closeMonitoring()
|
closeMonitoring()
|
||||||
closeFloorNavigation()
|
closeFloorNavigation()
|
||||||
closeNotifications()
|
closeNotifications()
|
||||||
|
closeListOfDetectors()
|
||||||
|
closeSensors()
|
||||||
}
|
}
|
||||||
toggleNavigationSubMenu()
|
toggleNavigationSubMenu()
|
||||||
}}
|
}}
|
||||||
@@ -344,6 +395,8 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
closeMonitoring()
|
closeMonitoring()
|
||||||
closeFloorNavigation()
|
closeFloorNavigation()
|
||||||
closeNotifications()
|
closeNotifications()
|
||||||
|
closeListOfDetectors()
|
||||||
|
closeSensors()
|
||||||
}
|
}
|
||||||
toggleNavigationSubMenu()
|
toggleNavigationSubMenu()
|
||||||
}
|
}
|
||||||
@@ -408,6 +461,8 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
closeMonitoring()
|
closeMonitoring()
|
||||||
closeFloorNavigation()
|
closeFloorNavigation()
|
||||||
closeNotifications()
|
closeNotifications()
|
||||||
|
closeListOfDetectors()
|
||||||
|
closeSensors()
|
||||||
}
|
}
|
||||||
// Убираем сайд-бар
|
// Убираем сайд-бар
|
||||||
toggleSidebar()
|
toggleSidebar()
|
||||||
|
|||||||
Reference in New Issue
Block a user