New api and zone management; highligh occlusion and highlighAll functionality; improved search in reports and alerts history + autofill; refactored alert panel

This commit is contained in:
iv_vuytsik
2025-12-25 03:10:21 +03:00
parent 31030f2997
commit ce7e39debf
36 changed files with 1562 additions and 472 deletions

View File

@@ -5,13 +5,14 @@ import { useRouter, useSearchParams } from 'next/navigation'
import Sidebar from '../../../components/ui/Sidebar'
import useNavigationStore from '../../store/navigationStore'
import DetectorList from '../../../components/alerts/DetectorList'
import AlertsList from '../../../components/alerts/AlertsList'
import ExportMenu from '../../../components/ui/ExportMenu'
import { useSession } from 'next-auth/react'
const AlertsPage: React.FC = () => {
const router = useRouter()
const searchParams = useSearchParams()
const { currentObject, setCurrentObject } = useNavigationStore()
const { currentObject, setCurrentObject, selectedDetector } = useNavigationStore()
const [selectedDetectors, setSelectedDetectors] = useState<number[]>([])
const { data: session } = useSession()
@@ -22,10 +23,9 @@ const AlertsPage: React.FC = () => {
timestamp: string
acknowledged: boolean
priority: string
detector_id?: number
detector_id?: string
detector_name?: string
location?: string
object?: string
}
const [alerts, setAlerts] = useState<AlertItem[]>([])
@@ -40,6 +40,13 @@ const AlertsPage: React.FC = () => {
}
}, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject])
// Auto-select detector when it comes from navigation store
useEffect(() => {
if (selectedDetector && !selectedDetectors.includes(selectedDetector.detector_id)) {
setSelectedDetectors(prev => [...prev, selectedDetector.detector_id])
}
}, [selectedDetector, selectedDetectors])
useEffect(() => {
const loadAlerts = async () => {
try {
@@ -55,7 +62,11 @@ const AlertsPage: React.FC = () => {
})
const data = Array.isArray(payload?.data) ? payload.data : (payload?.data?.alerts || [])
console.log('[AlertsPage] parsed alerts:', data)
setAlerts(data as AlertItem[])
console.log('[AlertsPage] Sample alert structure:', data[0])
console.log('[AlertsPage] Alerts with detector_id:', data.filter((alert: any) => alert.detector_id).length)
console.log('[AlertsPage] using transformed alerts:', data)
setAlerts(data)
} catch (e) {
console.error('Failed to load alerts:', e)
}
@@ -177,87 +188,15 @@ const AlertsPage: React.FC = () => {
objectId={objectId || undefined}
selectedDetectors={selectedDetectors}
onDetectorSelect={handleDetectorSelect}
initialSearchTerm={selectedDetector?.detector_id.toString() || ''}
/>
{/* История тревог */}
<div className="mt-6 bg-[#161824] rounded-[20px] p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold text-white">История тревог</h2>
<span className="text-sm text-gray-400">Всего: {alerts.length}</span>
</div>
<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>
{alerts.map((item) => (
<tr key={item.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">{item.detector_name || 'Детектор'}</div>
{item.detector_id ? (
<div className="text-sm text-gray-400">ID: {item.detector_id}</div>
) : null}
</td>
<td className="py-4">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: item.type === 'critical' ? '#b3261e' : item.type === 'warning' ? '#fd7c22' : '#00ff00' }}
></div>
<span className="text-sm text-gray-300">
{item.type === 'critical' ? 'Критический' : item.type === 'warning' ? 'Предупреждение' : 'Информация'}
</span>
</div>
</td>
<td className="py-4">
<div className="text-sm text-white">{item.message}</div>
</td>
<td className="py-4">
<div className="text-sm text-white">{item.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: item.priority === 'high' ? '#b3261e' : item.priority === 'medium' ? '#fd7c22' : '#00ff00' }}
>
{item.priority === 'high' ? 'Высокий' : item.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 ${
item.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'
}`}>
{item.acknowledged ? 'Да' : 'Нет'}
</span>
<button
onClick={() => handleAcknowledgeToggle(item.id)}
className="ml-2 inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-[#2a2e3e] text-white hover:bg-[#353a4d]"
>
{item.acknowledged ? 'Снять' : 'Подтвердить'}
</button>
</td>
<td className="py-4">
<div className="text-sm text-gray-300">{new Date(item.timestamp).toLocaleString('ru-RU')}</div>
</td>
</tr>
))}
{alerts.length === 0 && (
<tr>
<td colSpan={7} className="py-8 text-center text-gray-400">Записей не найдено</td>
</tr>
)}
</tbody>
</table>
</div>
<div className="mt-8">
<AlertsList
alerts={alerts}
onAcknowledgeToggle={handleAcknowledgeToggle}
initialSearchTerm={selectedDetector?.detector_id.toString() || ''}
/>
</div>
</div>
</div>

View File

@@ -107,6 +107,41 @@ const NavigationPage: React.FC = () => {
const [detectorsError, setDetectorsError] = useState<string | null>(null)
const [modelError, setModelError] = useState<string | null>(null)
const [isModelReady, setIsModelReady] = useState(false)
const [focusedSensorId, setFocusedSensorId] = useState<string | null>(null)
const [highlightAllSensors, setHighlightAllSensors] = useState(false)
useEffect(() => {
if (selectedDetector === null && selectedAlert === null) {
setFocusedSensorId(null);
}
}, [selectedDetector, selectedAlert]);
// Управление выделением всех сенсоров при открытии/закрытии меню Sensors
useEffect(() => {
console.log('[NavigationPage] showSensors changed:', showSensors, 'modelReady:', isModelReady)
if (showSensors && isModelReady) {
// При открытии меню Sensors - выделяем все сенсоры (только если модель готова)
console.log('[NavigationPage] Setting highlightAllSensors to TRUE')
setHighlightAllSensors(true)
setFocusedSensorId(null)
} else if (!showSensors) {
// При закрытии меню Sensors - сбрасываем выделение
console.log('[NavigationPage] Setting highlightAllSensors to FALSE')
setHighlightAllSensors(false)
}
}, [showSensors, isModelReady])
// Дополнительный эффект для задержки выделения сенсоров при открытии меню
useEffect(() => {
if (showSensors && isModelReady) {
const timer = setTimeout(() => {
console.log('[NavigationPage] Delayed highlightAllSensors to TRUE')
setHighlightAllSensors(true)
}, 500) // Задержка 500мс для полной инициализации модели
return () => clearTimeout(timer)
}
}, [showSensors, isModelReady])
const urlObjectId = searchParams.get('objectId')
const urlObjectTitle = searchParams.get('objectTitle')
@@ -133,10 +168,10 @@ const NavigationPage: React.FC = () => {
}, [selectedModelPath]);
useEffect(() => {
if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) {
setCurrentObject(urlObjectId, urlObjectTitle)
if (urlObjectId && (!currentObject.id || currentObject.id !== urlObjectId)) {
setCurrentObject(urlObjectId, urlObjectTitle ?? currentObject.title ?? undefined)
}
}, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject])
}, [urlObjectId, urlObjectTitle, currentObject.id, currentObject.title, setCurrentObject])
useEffect(() => {
const loadDetectors = async () => {
@@ -177,18 +212,28 @@ const NavigationPage: React.FC = () => {
return
}
if (selectedDetector?.detector_id === detector.detector_id && showDetectorMenu) {
setShowDetectorMenu(false)
setSelectedDetector(null)
if (selectedDetector?.serial_number === detector.serial_number && showDetectorMenu) {
closeDetectorMenu()
} else {
setSelectedDetector(detector)
setShowDetectorMenu(true)
setFocusedSensorId(detector.serial_number)
setShowAlertMenu(false)
setSelectedAlert(null)
// При открытии меню детектора - сбрасываем множественное выделение
setHighlightAllSensors(false)
}
}
const closeDetectorMenu = () => {
setShowDetectorMenu(false)
setSelectedDetector(null)
setFocusedSensorId(null)
setSelectedAlert(null)
// При закрытии меню детектора из Sensors - выделяем все сенсоры снова
if (showSensors) {
setHighlightAllSensors(true)
}
}
const handleNotificationClick = (notification: NotificationType) => {
@@ -209,6 +254,12 @@ const NavigationPage: React.FC = () => {
const closeAlertMenu = () => {
setShowAlertMenu(false)
setSelectedAlert(null)
setFocusedSensorId(null)
setSelectedDetector(null)
// При закрытии меню алерта из Sensors - выделяем все сенсоры снова
if (showSensors) {
setHighlightAllSensors(true)
}
}
const handleAlertClick = (alert: AlertType) => {
@@ -219,19 +270,86 @@ const NavigationPage: React.FC = () => {
)
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)
if (selectedAlert?.id === alert.id && showAlertMenu) {
closeAlertMenu()
} else {
setSelectedAlert(alert)
setShowAlertMenu(true)
setFocusedSensorId(detector.serial_number)
setShowDetectorMenu(false)
setSelectedDetector(null)
// При открытии меню алерта - сбрасываем множественное выделение
setHighlightAllSensors(false)
console.log('[NavigationPage] Showing AlertMenu for alert:', alert.detector_name)
}
} else {
console.warn('[NavigationPage] Could not find detector for alert:', alert.detector_id)
}
}
const handleSensorSelection = (serialNumber: string | null) => {
if (serialNumber === null) {
setFocusedSensorId(null);
closeDetectorMenu();
closeAlertMenu();
// If we're in Sensors menu and no sensor is selected, highlight all sensors
if (showSensors) {
setHighlightAllSensors(true);
}
return;
}
if (focusedSensorId === serialNumber) {
setFocusedSensorId(null);
closeDetectorMenu();
closeAlertMenu();
// If we're in Sensors menu and deselected the current sensor, highlight all sensors
if (showSensors) {
setHighlightAllSensors(true);
}
return;
}
// При выборе конкретного сенсора - сбрасываем множественное выделение
setHighlightAllSensors(false)
const detector = Object.values(detectorsData?.detectors || {}).find(
(d) => d.serial_number === serialNumber
);
if (detector) {
if (showFloorNavigation || showListOfDetectors) {
handleDetectorMenuClick(detector);
} else if (detector.notifications && detector.notifications.length > 0) {
const sortedNotifications = [...detector.notifications].sort((a, b) => {
const priorityOrder: { [key: string]: number } = { critical: 0, warning: 1, info: 2 };
return priorityOrder[a.priority.toLowerCase()] - priorityOrder[b.priority.toLowerCase()];
});
const notification = sortedNotifications[0];
const alert: AlertType = {
...notification,
detector_id: detector.detector_id,
detector_name: detector.name,
location: detector.location,
object: detector.object,
status: detector.status,
type: notification.type || 'info',
};
handleAlertClick(alert);
} else {
handleDetectorMenuClick(detector);
}
} else {
setFocusedSensorId(null);
closeDetectorMenu();
closeAlertMenu();
// If we're in Sensors menu and no valid detector found, highlight all sensors
if (showSensors) {
setHighlightAllSensors(true);
}
}
};
const getStatusText = (status: string) => {
const s = (status || '').toLowerCase()
switch (s) {
@@ -371,9 +489,9 @@ const NavigationPage: React.FC = () => {
</button>
<nav className="flex items-center gap-2 text-sm">
<span className="text-gray-400">Дашборд</span>
<span className="text-gray-600">/</span>
<span className="text-gray-600">{'/'}</span>
<span className="text-white">{objectTitle || 'Объект'}</span>
<span className="text-gray-600">/</span>
<span className="text-gray-600">{'/'}</span>
<span className="text-white">Навигация</span>
</nav>
</div>
@@ -414,7 +532,7 @@ const NavigationPage: React.FC = () => {
</div>
</div>
) : (
<ModelViewer
<ModelViewer
key={selectedModelPath || 'no-model'}
modelPath={selectedModelPath}
onSelectModel={(path) => {
@@ -425,7 +543,11 @@ const NavigationPage: React.FC = () => {
}}
onModelLoaded={handleModelLoaded}
onError={handleModelError}
focusSensorId={selectedDetector?.serial_number ?? selectedAlert?.detector_id?.toString() ?? null}
activeMenu={showSensors ? 'sensors' : showFloorNavigation ? 'floor' : showListOfDetectors ? 'detectors' : null}
focusSensorId={focusedSensorId}
highlightAllSensors={highlightAllSensors}
isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors}
onSensorPick={handleSensorSelection}
renderOverlay={({ anchor }) => (
<>
{selectedAlert && showAlertMenu && anchor ? (
@@ -449,12 +571,12 @@ const NavigationPage: React.FC = () => {
) : null}
</>
)}
/>
)}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -10,11 +10,27 @@ import { useRouter } from 'next/navigation'
const transformRawToObjectData = (raw: any): ObjectData => {
const rawId = raw?.id ?? raw?.object_id ?? raw?.uuid ?? raw?.name
const object_id = typeof rawId === 'number' ? `object_${rawId}` : String(rawId ?? '')
// Если объект имеет числовой идентификатор, возвращаем его в виде строки с префиксом 'object_'
const deriveTitle = (): string => {
const t = (raw?.title || '').toString().trim()
if (t) return t
const idStr = String(rawId ?? '').toString()
const numMatch = typeof rawId === 'number'
? rawId
: (() => { const m = idStr.match(/\d+/); return m ? Number(m[0]) : undefined })()
if (typeof numMatch === 'number' && !Number.isNaN(numMatch)) {
return `Объект ${numMatch}`
}
// Если объект не имеет числовой идентификатор, возвращаем его строковый идентификатор
return idStr ? `Объект ${idStr}` : `Объект ${object_id}`
}
return {
object_id,
title: raw?.title ?? `Объект ${object_id}`,
title: deriveTitle(),
description: raw?.description ?? `Описание объекта ${raw?.title ?? object_id}`,
image: raw?.image ?? '/images/test_image.png',
image: raw?.image ?? null,
location: raw?.location ?? raw?.address ?? 'Не указано',
floors: Number(raw?.floors ?? 0),
area: String(raw?.area ?? ''),

View File

@@ -13,7 +13,7 @@ const ReportsPage: React.FC = () => {
const router = useRouter()
const searchParams = useSearchParams()
const { data: session } = useSession()
const { currentObject, setCurrentObject } = useNavigationStore()
const { currentObject, setCurrentObject, selectedDetector } = useNavigationStore()
const [selectedDetectors, setSelectedDetectors] = useState<number[]>([])
const [detectorsData, setDetectorsData] = useState<any>({ detectors: {} })
@@ -28,6 +28,13 @@ const ReportsPage: React.FC = () => {
}
}, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject])
// Автовыбор detector id из стора
useEffect(() => {
if (selectedDetector && !selectedDetectors.includes(selectedDetector.detector_id)) {
setSelectedDetectors(prev => [...prev, selectedDetector.detector_id])
}
}, [selectedDetector, selectedDetectors])
useEffect(() => {
const loadDetectors = async () => {
try {
@@ -139,15 +146,21 @@ const ReportsPage: React.FC = () => {
<ExportMenu onExport={handleExport} />
</div>
{/* Selection table to choose detectors to include in the report */}
<div className="mb-6">
<DetectorList objectId={objectId || undefined} selectedDetectors={selectedDetectors} onDetectorSelect={handleDetectorSelect} />
<DetectorList
objectId={objectId || undefined}
selectedDetectors={selectedDetectors}
onDetectorSelect={handleDetectorSelect}
initialSearchTerm={selectedDetector?.detector_id.toString() || ''}
/>
</div>
{/* Existing notifications-based list */}
<div className="mt-6">
<ReportsList detectorsData={detectorsData} />
<ReportsList
detectorsData={detectorsData}
initialSearchTerm={selectedDetector?.detector_id.toString() || ''}
/>
</div>
</div>
</div>