Files
aerbim-ht-monitor/frontend/app/store/navigationStore.ts

499 lines
18 KiB
TypeScript
Raw Permalink 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.
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { Zone } from '@/app/types'
export 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 NotificationType {
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 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 {
currentObject: { id: string | undefined; title: string | undefined }
navigationHistory: string[]
currentSubmenu: string | null
currentModelPath: string | null
// Состояния Зон
currentZones: Zone[]
zonesCache: Record<string, Zone[]>
zonesLoading: boolean
zonesError: string | null
showMonitoring: boolean
showFloorNavigation: boolean
showNotifications: boolean
showListOfDetectors: boolean
showSensors: boolean
showSensorHighlights: boolean
selectedDetector: DetectorType | null
showDetectorMenu: boolean
selectedNotification: NotificationType | null
showNotificationDetectorInfo: boolean
selectedAlert: AlertType | null
showAlertMenu: boolean
setCurrentObject: (id: string | undefined, title: string | undefined) => void
clearCurrentObject: () => void
addToHistory: (path: string) => void
goBack: () => string | null
setCurrentModelPath: (path: string) => void
setCurrentSubmenu: (submenu: string | null) => void
clearSubmenu: () => void
// Действия с зонами
loadZones: (objectId: string) => Promise<void>
setZones: (zones: Zone[]) => void
clearZones: () => void
openMonitoring: () => void
closeMonitoring: () => void
openFloorNavigation: () => void
closeFloorNavigation: () => void
openNotifications: () => void
closeNotifications: () => void
openListOfDetectors: () => void
closeListOfDetectors: () => void
openSensors: () => void
closeSensors: () => void
toggleSensorHighlights: () => void
setSensorHighlights: (show: boolean) => void
closeAllMenus: () => void
clearSelections: () => void
setSelectedDetector: (detector: DetectorType | null) => void
setShowDetectorMenu: (show: boolean) => void
setSelectedNotification: (notification: NotificationType | null) => void
setShowNotificationDetectorInfo: (show: boolean) => void
setSelectedAlert: (alert: AlertType | null) => void
setShowAlertMenu: (show: boolean) => void
isOnNavigationPage: () => boolean
getCurrentRoute: () => string | null
getActiveSidebarItem: () => number
// Навигация к датчику на 3D-модели
navigateToSensor: (sensorSerialNumber: string, floor: number | null, viewType: 'building' | 'floor') => Promise<{ sensorSerialNumber: string; modelPath: string } | null>
}
const useNavigationStore = create<NavigationStore>()(
persist(
(set, get) => ({
currentObject: {
id: undefined,
title: undefined,
},
navigationHistory: [],
currentSubmenu: null,
currentModelPath: null,
currentZones: [],
zonesCache: {},
zonesLoading: false,
zonesError: null,
showMonitoring: false,
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
showSensors: false,
showSensorHighlights: true,
selectedDetector: null,
showDetectorMenu: false,
selectedNotification: null,
showNotificationDetectorInfo: false,
selectedAlert: null,
showAlertMenu: false,
setCurrentObject: (id: string | undefined, title: string | undefined) =>
set({ currentObject: { id, title } }),
clearCurrentObject: () =>
set({ currentObject: { id: undefined, title: undefined } }),
setCurrentModelPath: (path: string) => set({ currentModelPath: path }),
addToHistory: (path: string) => {
const { navigationHistory } = get()
const newHistory = [...navigationHistory, path]
if (newHistory.length > 10) {
newHistory.shift()
}
set({ navigationHistory: newHistory })
},
goBack: () => {
const { navigationHistory } = get()
if (navigationHistory.length > 1) {
const newHistory = [...navigationHistory]
newHistory.pop()
const previousPage = newHistory.pop()
set({ navigationHistory: newHistory })
return previousPage || null
}
return null
},
setCurrentSubmenu: (submenu: string | null) =>
set({ currentSubmenu: submenu }),
clearSubmenu: () =>
set({ currentSubmenu: null }),
loadZones: async (objectId: string) => {
const cache = get().zonesCache
const cached = cache[objectId]
const hasCached = Array.isArray(cached) && cached.length > 0
if (hasCached) {
// Показываем кэшированные зоны сразу, но обновляем в фоне
set({ currentZones: cached, zonesLoading: true, zonesError: null })
} else {
set({ zonesLoading: true, zonesError: null })
}
try {
const res = await fetch(`/api/get-zones?objectId=${encodeURIComponent(objectId)}`, { cache: 'no-store' })
const text = await res.text()
let payload: string | Record<string, unknown>
try { payload = JSON.parse(text) } catch { payload = text }
if (!res.ok) throw new Error(typeof payload === 'string' ? payload : (payload?.error as string || 'Не удалось получить зоны'))
const zones: Zone[] = typeof payload === 'string' ? [] :
Array.isArray(payload?.data) ? payload.data as Zone[] :
(payload?.data && typeof payload.data === 'object' && 'zones' in payload.data ? (payload.data as { zones?: Zone[] }).zones :
payload?.zones ? payload.zones as Zone[] : []) || []
const normalized = zones.map((z) => ({
...z,
image_path: z.image_path ?? null,
}))
set((state) => ({
currentZones: normalized,
zonesCache: { ...state.zonesCache, [objectId]: normalized },
zonesLoading: false,
zonesError: null,
}))
} catch (e: unknown) {
set({ zonesLoading: false, zonesError: (e as Error)?.message || 'Ошибка при загрузке зон' })
}
},
setZones: (zones: Zone[]) => set({ currentZones: zones }),
clearZones: () => set({ currentZones: [] }),
openMonitoring: () => {
set({
showMonitoring: true,
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
currentSubmenu: 'monitoring',
showDetectorMenu: false,
selectedDetector: null,
showNotificationDetectorInfo: false,
selectedNotification: null,
zonesError: null // Очищаем ошибку зон при открытии мониторинга
})
const objId = get().currentObject.id
if (objId) {
// Вызываем загрузку зон сразу, но обновляем в фоне
get().loadZones(objId)
}
},
closeMonitoring: () => set({
showMonitoring: false,
currentSubmenu: null
}),
openFloorNavigation: () => set({
showFloorNavigation: true,
showMonitoring: false,
showNotifications: false,
showListOfDetectors: false,
currentSubmenu: 'floors',
showNotificationDetectorInfo: false,
selectedNotification: null
}),
closeFloorNavigation: () => set({
showFloorNavigation: false,
showDetectorMenu: false,
selectedDetector: null,
currentSubmenu: null
}),
openNotifications: () => set({
showNotifications: true,
showMonitoring: false,
showFloorNavigation: false,
showListOfDetectors: false,
currentSubmenu: 'notifications',
showDetectorMenu: false,
selectedDetector: null
}),
closeNotifications: () => set({
showNotifications: false,
showNotificationDetectorInfo: false,
selectedNotification: 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
}),
toggleSensorHighlights: () => set((state) => ({ showSensorHighlights: !state.showSensorHighlights })),
setSensorHighlights: (show: boolean) => set({ showSensorHighlights: show }),
closeAllMenus: () => {
set({
showMonitoring: false,
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
showSensors: false,
currentSubmenu: null,
});
get().clearSelections();
},
clearSelections: () => set({
selectedDetector: null,
showDetectorMenu: false,
selectedAlert: null,
showAlertMenu: false,
}),
setSelectedDetector: (detector: DetectorType | null) => set({ selectedDetector: detector }),
setShowDetectorMenu: (show: boolean) => set({ showDetectorMenu: show }),
setSelectedNotification: (notification: NotificationType | null) => set({ selectedNotification: notification }),
setShowNotificationDetectorInfo: (show: boolean) => set({ showNotificationDetectorInfo: show }),
setSelectedAlert: (alert: AlertType | null) => set({ selectedAlert: alert }),
setShowAlertMenu: (show: boolean) => set({ showAlertMenu: show }),
isOnNavigationPage: () => {
const { navigationHistory } = get()
const currentRoute = navigationHistory[navigationHistory.length - 1]
return currentRoute === '/navigation'
},
getCurrentRoute: () => {
const { navigationHistory } = get()
return navigationHistory[navigationHistory.length - 1] || null
},
getActiveSidebarItem: () => {
const { showMonitoring, showFloorNavigation, showNotifications, showListOfDetectors, showSensors } = get()
if (showMonitoring) return 3 // Зоны Мониторинга
if (showFloorNavigation) return 4 // Навигация по этажам
if (showNotifications) return 5 // Уведомления
if (showListOfDetectors) return 7 // Список датчиков
if (showSensors) return 8 // Сенсоры
return 2 // Навигация (базовая)
},
// Навигация к датчику на 3D-модели
navigateToSensor: async (sensorSerialNumber: string, floor: number | null, viewType: 'building' | 'floor') => {
const { currentObject, loadZones } = get()
if (!currentObject.id) {
console.error('[navigateToSensor] No current object selected')
return null
}
// Загружаем зоны для объекта (из кэша или API)
await loadZones(currentObject.id)
const { currentZones } = get()
if (!currentZones || currentZones.length === 0) {
console.error('[navigateToSensor] No zones available for object', currentObject.id)
return null
}
let targetZone: Zone | undefined
if (viewType === 'building') {
// Для общего вида здания - ищем самую верхнюю зону (первую в списке)
targetZone = currentZones[0]
console.log('[navigateToSensor] Building view - selected first zone:', targetZone?.name)
} else if (viewType === 'floor') {
// Для вида на этаже - ищем зону, где есть этот датчик (исключая order=0)
// Фильтруем зоны: исключаем общий план (order=0)
const floorZones = currentZones.filter(z => z.order !== 0 && z.model_path)
console.log('[navigateToSensor] Searching in floor zones (excluding order=0):', floorZones.length)
console.log('[navigateToSensor] Floor zones:', floorZones.map(z => ({ id: z.id, name: z.name, order: z.order, floor: z.floor })))
console.log('[navigateToSensor] Looking for sensor:', sensorSerialNumber)
// Загружаем датчики для каждой зоны и ищем нужный
for (const zone of floorZones) {
try {
console.log(`[navigateToSensor] Checking zone: ${zone.name} (id: ${zone.id}, order: ${zone.order}, floor: ${zone.floor})`)
const res = await fetch(`/api/get-detectors?zone_id=${zone.id}`, { cache: 'no-store' })
if (!res.ok) {
console.warn(`[navigateToSensor] API request failed for zone ${zone.id}:`, res.status, res.statusText)
continue
}
const payload = await res.json()
const data = payload?.data ?? payload
const detectorsObj = (data?.detectors ?? {}) as Record<string, any>
const detectorsList = Object.values(detectorsObj)
console.log(`[navigateToSensor] Zone ${zone.name} has ${detectorsList.length} detectors:`, detectorsList.map((d: any) => d.serial_number || d.name))
// Проверяем есть ли датчик в этой зоне
const hasSensor = detectorsList.some((d: any) =>
d.serial_number === sensorSerialNumber ||
d.name === sensorSerialNumber
)
console.log(`[navigateToSensor] Sensor ${sensorSerialNumber} found in zone ${zone.name}:`, hasSensor)
if (hasSensor) {
targetZone = zone
console.log('[navigateToSensor] ✅ FOUND! Selected zone:', zone.name, 'zoneId:', zone.id, 'model_path:', zone.model_path)
break
}
} catch (e) {
console.error('[navigateToSensor] Failed to load detectors for zone:', zone.id, e)
continue
}
}
// Fallback на общий вид, если не нашли зону этажа
if (!targetZone) {
console.warn(`[navigateToSensor] No floor zone found with sensor ${sensorSerialNumber}, falling back to building view`)
targetZone = currentZones[0]
}
}
if (!targetZone || !targetZone.model_path) {
console.error('[navigateToSensor] No valid zone with model_path found')
return null
}
// Устанавливаем состояние для навигации
set({
currentModelPath: targetZone.model_path,
// Открываем Зоны контроля (Monitoring) - она автоматически закроется после загрузки модели
showMonitoring: true,
// Закрываем остальные меню
showFloorNavigation: false,
showNotifications: false,
showListOfDetectors: false,
// НЕ закрываем showSensors - оставляем как есть для подсветки датчиков
// showSensors: false, <- Убрали!
showDetectorMenu: false,
showAlertMenu: false,
selectedDetector: null,
selectedAlert: null,
})
console.log('[navigateToSensor] Navigation prepared:', {
sensorSerialNumber,
floor,
viewType,
modelPath: targetZone.model_path,
zoneName: targetZone.name,
zoneId: targetZone.id
})
// Возвращаем объект с sensorSerialNumber и modelPath
return {
sensorSerialNumber,
modelPath: targetZone.model_path
}
}
}),
{
name: 'navigation-store',
}
)
)
export default useNavigationStore