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 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 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 } const useNavigationStore = create()( 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 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') { // Для вида на этаже - ищем зону, где есть этот датчик // Сначала проверяем зоны с sensors массивом for (const zone of currentZones) { if (zone.sensors && Array.isArray(zone.sensors)) { const hasSensor = zone.sensors.some(s => s.serial_number === sensorSerialNumber || s.name === sensorSerialNumber ) if (hasSensor) { targetZone = zone console.log('[navigateToSensor] Found sensor in zone:', zone.name, 'sensors:', zone.sensors.length) break } } } // Если не нашли по sensors, пробуем по floor if (!targetZone && floor !== null) { // Ищем зоны с соответствующим floor (кроме общего вида) const floorZones = currentZones.filter(z => z.floor === floor && z.order !== 0 && z.model_path ) if (floorZones.length > 0) { targetZone = floorZones[0] console.log('[navigateToSensor] Found zone by floor:', targetZone.name, 'floor:', floor) } } // Fallback на общий вид, если не нашли зону этажа if (!targetZone) { console.warn(`[navigateToSensor] No zone found with sensor ${sensorSerialNumber} or floor ${floor}, 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 }) // Возвращаем serial_number для установки focusedSensorId в компоненте return sensorSerialNumber } }), { name: 'navigation-store', } ) ) export default useNavigationStore