From 4d6b7b48d73aa8c433d5112fa255d1043ae0d75d Mon Sep 17 00:00:00 2001 From: iv_vuytsik Date: Fri, 5 Sep 2025 03:16:17 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5?= =?UTF-8?q?=D0=B9=D1=81=D0=B0=20=D1=84=D1=80=D0=BE=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.gitignore | 4 + frontend/app/(protected)/alerts/page.tsx | 103 ++++ frontend/app/(protected)/dashboard/page.tsx | 26 +- frontend/app/(protected)/layout.tsx | 13 + frontend/app/(protected)/model/page.tsx | 25 +- frontend/app/(protected)/navigation/page.tsx | 241 +++++++++ frontend/app/(protected)/objects/page.tsx | 125 ++++- frontend/app/(protected)/reports/page.tsx | 85 ++++ frontend/app/api/cache-mesh-data/route.ts | 42 -- frontend/app/api/get-detectors-data/route.ts | 28 ++ frontend/app/globals.css | 31 ++ frontend/app/store/navigationStore.ts | 207 ++++++++ frontend/app/store/uiStore.ts | 31 ++ frontend/components/alerts/DetectorList.tsx | 246 +++++++++ frontend/components/dashboard/AreaChart.tsx | 48 ++ frontend/components/dashboard/BarChart.tsx | 62 +++ frontend/components/dashboard/ChartCard.tsx | 41 ++ frontend/components/dashboard/Dashboard.tsx | 263 ++++++++++ .../components/dashboard/DetectorChart.tsx | 103 ++++ .../components/{ => model}/ModelViewer.tsx | 81 +-- .../components/navigation/DetectorMenu.tsx | 97 ++++ .../components/navigation/FloorNavigation.tsx | 206 ++++++++ frontend/components/navigation/Monitoring.tsx | 69 +++ .../NotificationDetectorInfo.tsx | 175 +++++++ .../notifications/Notifications.tsx | 134 +++++ frontend/components/objects/ObjectCard.tsx | 103 ++++ frontend/components/objects/ObjectGallery.tsx | 102 ++++ frontend/components/reports/ReportsList.tsx | 288 +++++++++++ frontend/components/ui/ExportMenu.tsx | 54 ++ frontend/components/ui/LoadingSpinner.tsx | 72 +++ frontend/components/ui/Sidebar.tsx | 474 ++++++++++++++++++ frontend/data/detectors.json | 243 +++++++++ frontend/public/images/test_image.png | Bin 0 -> 90978 bytes frontend/services/navigationService.ts | 92 ++++ frontend/utils/meshCache.ts | 168 ------- 35 files changed, 3806 insertions(+), 276 deletions(-) create mode 100644 frontend/app/(protected)/alerts/page.tsx create mode 100644 frontend/app/(protected)/navigation/page.tsx create mode 100644 frontend/app/(protected)/reports/page.tsx delete mode 100644 frontend/app/api/cache-mesh-data/route.ts create mode 100644 frontend/app/api/get-detectors-data/route.ts create mode 100644 frontend/app/store/navigationStore.ts create mode 100644 frontend/app/store/uiStore.ts create mode 100644 frontend/components/alerts/DetectorList.tsx create mode 100644 frontend/components/dashboard/AreaChart.tsx create mode 100644 frontend/components/dashboard/BarChart.tsx create mode 100644 frontend/components/dashboard/ChartCard.tsx create mode 100644 frontend/components/dashboard/Dashboard.tsx create mode 100644 frontend/components/dashboard/DetectorChart.tsx rename frontend/components/{ => model}/ModelViewer.tsx (72%) create mode 100644 frontend/components/navigation/DetectorMenu.tsx create mode 100644 frontend/components/navigation/FloorNavigation.tsx create mode 100644 frontend/components/navigation/Monitoring.tsx create mode 100644 frontend/components/notifications/NotificationDetectorInfo.tsx create mode 100644 frontend/components/notifications/Notifications.tsx create mode 100644 frontend/components/objects/ObjectCard.tsx create mode 100644 frontend/components/objects/ObjectGallery.tsx create mode 100644 frontend/components/reports/ReportsList.tsx create mode 100644 frontend/components/ui/ExportMenu.tsx create mode 100644 frontend/components/ui/LoadingSpinner.tsx create mode 100644 frontend/components/ui/Sidebar.tsx create mode 100644 frontend/data/detectors.json create mode 100644 frontend/public/images/test_image.png create mode 100644 frontend/services/navigationService.ts delete mode 100644 frontend/utils/meshCache.ts diff --git a/frontend/.gitignore b/frontend/.gitignore index 5ef6a52..d83c234 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Include data and models folders +!/data/ +!/public/models/ diff --git a/frontend/app/(protected)/alerts/page.tsx b/frontend/app/(protected)/alerts/page.tsx new file mode 100644 index 0000000..96d3a2b --- /dev/null +++ b/frontend/app/(protected)/alerts/page.tsx @@ -0,0 +1,103 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Sidebar from '../../../components/ui/Sidebar' +import useNavigationStore from '../../store/navigationStore' +import DetectorList from '../../../components/alerts/DetectorList' +import ExportMenu from '../../../components/ui/ExportMenu' + +const AlertsPage: React.FC = () => { + const router = useRouter() + const searchParams = useSearchParams() + const { currentObject, setCurrentObject } = useNavigationStore() + const [selectedDetectors, setSelectedDetectors] = useState([]) + + const urlObjectId = searchParams.get('objectId') + const urlObjectTitle = searchParams.get('objectTitle') + const objectId = currentObject.id || urlObjectId + const objectTitle = currentObject.title || urlObjectTitle + + useEffect(() => { + if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) { + setCurrentObject(urlObjectId, urlObjectTitle) + } + }, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject]) + + const handleBackClick = () => { + router.push('/dashboard') + } + + const handleDetectorSelect = (detectorId: number, selected: boolean) => { + if (selected) { + setSelectedDetectors(prev => [...prev, detectorId]) + } else { + setSelectedDetectors(prev => prev.filter(id => id !== detectorId)) + } + } + + const handleExport = (format: 'csv' | 'pdf') => { + // TODO: добавить функционал по экспорту + console.log(`Exporting ${selectedDetectors.length} items as ${format}`) + } + + + + return ( +
+ + +
+
+
+ + +
+
+ +
+
+
+

Уведомления и тревоги

+ +
+ {/* Кол-во выбранных объектов */} + {selectedDetectors.length > 0 && ( + + Выбрано: {selectedDetectors.length} + + )} + + +
+
+ + +
+
+
+
+ ) +} + +export default AlertsPage \ No newline at end of file diff --git a/frontend/app/(protected)/dashboard/page.tsx b/frontend/app/(protected)/dashboard/page.tsx index 983ebb8..ca7cb2d 100644 --- a/frontend/app/(protected)/dashboard/page.tsx +++ b/frontend/app/(protected)/dashboard/page.tsx @@ -1,9 +1,23 @@ -import React from 'react' +'use client' -const page = () => { - return ( -
page
- ) +import React, { useEffect } from 'react' +import { useSearchParams } from 'next/navigation' +import Dashboard from '../../../components/dashboard/Dashboard' +import useNavigationStore from '../../store/navigationStore' + +const DashboardPage = () => { + const searchParams = useSearchParams() + const { currentObject, setCurrentObject } = useNavigationStore() + + const urlObjectId = searchParams.get('objectId') + const urlObjectTitle = searchParams.get('objectTitle') + useEffect(() => { + if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) { + setCurrentObject(urlObjectId, urlObjectTitle) + } + }, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject]) + + return } -export default page \ No newline at end of file +export default DashboardPage \ No newline at end of file diff --git a/frontend/app/(protected)/layout.tsx b/frontend/app/(protected)/layout.tsx index e69de29..6ff9672 100644 --- a/frontend/app/(protected)/layout.tsx +++ b/frontend/app/(protected)/layout.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +export default function ProtectedLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( +
+ {children} +
+ ) +} \ No newline at end of file diff --git a/frontend/app/(protected)/model/page.tsx b/frontend/app/(protected)/model/page.tsx index 17a6231..027d2da 100644 --- a/frontend/app/(protected)/model/page.tsx +++ b/frontend/app/(protected)/model/page.tsx @@ -1,16 +1,9 @@ 'use client' import React, { useState } from 'react' -import ModelViewer from '@/components/ModelViewer' +import ModelViewer from '@/components/model/ModelViewer' export default function Home() { - const [modelInfo, setModelInfo] = useState<{ - meshes: unknown[] - boundingBox: { - min: { x: number; y: number; z: number } - max: { x: number; y: number; z: number } - } - } | null>(null) const [error, setError] = useState(null) const handleModelLoaded = (data: { @@ -20,14 +13,12 @@ export default function Home() { max: { x: number; y: number; z: number } } }) => { - setModelInfo(data) setError(null) console.log('Model loaded successfully:', data) } const handleError = (errorMessage: string) => { setError(errorMessage) - setModelInfo(null) } return ( @@ -42,19 +33,7 @@ export default function Home() {
Error: {error}
- )} - - {modelInfo && ( -
-

EXPO Building Model

- -
-
🖱️ Left click + drag: Rotate
-
🖱️ Right click + drag: Pan
-
🖱️ Scroll: Zoom in/out
-
-
- )} + )} ) } diff --git a/frontend/app/(protected)/navigation/page.tsx b/frontend/app/(protected)/navigation/page.tsx new file mode 100644 index 0000000..f59ff96 --- /dev/null +++ b/frontend/app/(protected)/navigation/page.tsx @@ -0,0 +1,241 @@ +'use client' + +import React, { useEffect, useCallback } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Sidebar from '../../../components/ui/Sidebar' +import useNavigationStore from '../../store/navigationStore' +import Monitoring from '../../../components/navigation/Monitoring' +import FloorNavigation from '../../../components/navigation/FloorNavigation' +import DetectorMenu from '../../../components/navigation/DetectorMenu' +import Notifications from '../../../components/notifications/Notifications' +import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo' +import ModelViewer from '../../../components/model/ModelViewer' +import detectorsData from '../../../data/detectors.json' + +interface DetectorType { + detector_id: number + name: string + object: string + status: string + checked: boolean + type: string + location: string + floor: number + 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 +} + +const NavigationPage: React.FC = () => { + const router = useRouter() + const searchParams = useSearchParams() + const { + currentObject, + setCurrentObject, + showMonitoring, + showFloorNavigation, + showNotifications, + selectedDetector, + showDetectorMenu, + selectedNotification, + showNotificationDetectorInfo, + closeMonitoring, + closeFloorNavigation, + closeNotifications, + setSelectedDetector, + setShowDetectorMenu, + setSelectedNotification, + setShowNotificationDetectorInfo + } = useNavigationStore() + + const urlObjectId = searchParams.get('objectId') + const urlObjectTitle = searchParams.get('objectTitle') + const objectId = currentObject.id || urlObjectId + const objectTitle = currentObject.title || urlObjectTitle + + const handleModelLoaded = useCallback(() => { + }, []) + + const handleModelError = useCallback((error: string) => { + console.error('Model loading error:', error) + }, []) + + useEffect(() => { + if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) { + setCurrentObject(urlObjectId, urlObjectTitle) + } + }, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject]) + + const handleBackClick = () => { + router.push('/dashboard') + } + + const handleDetectorMenuClick = (detector: DetectorType) => { + if (selectedDetector?.detector_id === detector.detector_id && showDetectorMenu) { + setShowDetectorMenu(false) + setSelectedDetector(null) + } else { + setSelectedDetector(detector) + setShowDetectorMenu(true) + } + } + + const closeDetectorMenu = () => { + setShowDetectorMenu(false) + setSelectedDetector(null) + } + + const handleNotificationClick = (notification: NotificationType) => { + if (selectedNotification?.id === notification.id && showNotificationDetectorInfo) { + setShowNotificationDetectorInfo(false) + setSelectedNotification(null) + } else { + setSelectedNotification(notification) + setShowNotificationDetectorInfo(true) + } + } + + const closeNotificationDetectorInfo = () => { + setShowNotificationDetectorInfo(false) + setSelectedNotification(null) + } + + const getStatusText = (status: string) => { + switch (status) { + case 'active': return 'Активен' + case 'inactive': return 'Неактивен' + case 'error': return 'Ошибка' + case 'maintenance': return 'Обслуживание' + default: return 'Неизвестно' + } + } + + return ( +
+ + +
+ + {showMonitoring && ( +
+
+ +
+
+ )} + + {showFloorNavigation && ( +
+
+ +
+
+ )} + + {showNotifications && ( +
+
+ +
+
+ )} + + {showNotifications && showNotificationDetectorInfo && selectedNotification && (() => { + const detectorData = Object.values(detectorsData.detectors).find( + detector => detector.detector_id === selectedNotification.detector_id + ); + return detectorData ? ( +
+
+ +
+
+ ) : null; + })()} + + {showFloorNavigation && showDetectorMenu && selectedDetector && ( + + )} + +
+
+
+ + +
+ + +
+
+ +
+
+ +
+
+
+
+ ) +} + +export default NavigationPage \ No newline at end of file diff --git a/frontend/app/(protected)/objects/page.tsx b/frontend/app/(protected)/objects/page.tsx index 983ebb8..ef574ca 100644 --- a/frontend/app/(protected)/objects/page.tsx +++ b/frontend/app/(protected)/objects/page.tsx @@ -1,9 +1,126 @@ -import React from 'react' +'use client' + +import React, { useState, useEffect } from 'react' +import ObjectGallery from '../../../components/objects/ObjectGallery' +import { ObjectData } from '../../../components/objects/ObjectCard' +import Sidebar from '../../../components/ui/Sidebar' +import detectorsData from '../../../data/detectors.json' + +// Интерфейс для данных объекта из JSON +interface RawObjectData { + name: string + title: string + description: string + image?: string + location: string + address: string + floors: number + area: number + type?: string + status?: string + zones: Array<{ + zone_id: string + name: string + detectors: number[] + }> +} + +// Функция для преобразования данных объекта из JSON +const transformObjectToObjectData = (objectId: string, objectData: RawObjectData): ObjectData => { + return { + object_id: objectId, + title: objectData.title || `Объект ${objectId}`, + description: objectData.description || `Описание объекта ${objectData.title || objectId}`, + image: objectData.image || '/images/default-object.jpg', + location: objectData.location || 'Не указано', + floors: objectData.floors, + area: objectData.area.toString(), + type: objectData.type || 'object', + status: objectData.status || 'active' + } +} + +const ObjectsPage: React.FC = () => { + const [objects, setObjects] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [selectedObjectId, setSelectedObjectId] = useState(null) + + useEffect(() => { + const loadData = () => { + try { + setLoading(true) + + if (detectorsData.objects) { + const transformedObjects = Object.entries(detectorsData.objects).map( + ([objectId, objectData]) => transformObjectToObjectData(objectId, objectData) + ) + setObjects(transformedObjects) + } else { + throw new Error('Не удалось получить данные объектов') + } + } catch (err) { + console.error('Ошибка при загрузке данных:', err) + setError(err instanceof Error ? err.message : 'Произошла неизвестная ошибка') + } finally { + setLoading(false) + } + } + + loadData() + }, []) + + const handleObjectSelect = (objectId: string) => { + console.log('Object selected:', objectId) + setSelectedObjectId(objectId) + } + + if (loading) { + return ( +
+
+
+

Загрузка объектов...

+
+
+ ) + } + + if (error) { + return ( +
+
+
+ + + +
+

Ошибка загрузки

+

{error}

+ +
+
+ ) + } -const page = () => { return ( -
page
+
+ +
+ +
+
) } -export default page \ No newline at end of file +export default ObjectsPage \ No newline at end of file diff --git a/frontend/app/(protected)/reports/page.tsx b/frontend/app/(protected)/reports/page.tsx new file mode 100644 index 0000000..2b81eda --- /dev/null +++ b/frontend/app/(protected)/reports/page.tsx @@ -0,0 +1,85 @@ +'use client' + +import React, { useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Sidebar from '../../../components/ui/Sidebar' +import useNavigationStore from '../../store/navigationStore' +import ReportsList from '../../../components/reports/ReportsList' +import ExportMenu from '../../../components/ui/ExportMenu' +import detectorsData from '../../../data/detectors.json' + +const ReportsPage: React.FC = () => { + const router = useRouter() + const searchParams = useSearchParams() + const { currentObject, setCurrentObject } = useNavigationStore() + + const urlObjectId = searchParams.get('objectId') + const urlObjectTitle = searchParams.get('objectTitle') + const objectId = currentObject.id || urlObjectId + const objectTitle = currentObject.title || urlObjectTitle + + useEffect(() => { + if (urlObjectId && urlObjectTitle && (!currentObject.id || currentObject.id !== urlObjectId)) { + setCurrentObject(urlObjectId, urlObjectTitle) + } + }, [urlObjectId, urlObjectTitle, currentObject.id, setCurrentObject]) + + const handleBackClick = () => { + router.push('/dashboard') + } + + const handleExport = (format: 'csv' | 'pdf') => { + // TODO: добавить функционал по экспорту отчетов + console.log(`Exporting reports as ${format}`) + } + + + + return ( +
+ + +
+
+
+ + +
+
+ +
+
+
+

Отчеты по датчикам

+ + +
+ + +
+
+
+
+ ) +} + +export default ReportsPage \ No newline at end of file diff --git a/frontend/app/api/cache-mesh-data/route.ts b/frontend/app/api/cache-mesh-data/route.ts deleted file mode 100644 index 25343f2..0000000 --- a/frontend/app/api/cache-mesh-data/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { writeFile, mkdir } from 'fs/promises' -import { join } from 'path' - -export async function POST(request: NextRequest) { - try { - const body = await request.json() - console.log('API: Received request with fileName:', body.fileName) - - const { fileName, data } = body - - const dataDir = join(process.cwd(), 'data') - const filePath = join(dataDir, fileName) - - console.log('API: Writing to:', filePath) - - try { - await mkdir(dataDir, { recursive: true }) - console.log('API: Data directory created/verified') - } catch (error) { - console.log('API: Data directory already exists') - } - - await writeFile(filePath, JSON.stringify(data, null, 2), 'utf8') - console.log('API: File written successfully') - - return NextResponse.json({ - success: true, - message: 'Mesh data cached successfully', - fileName - }) - } catch (error) { - console.error('API: Error caching mesh data:', error) - return NextResponse.json( - { - success: false, - error: `Failed to cache mesh data: ${error}` - }, - { status: 500 } - ) - } -} \ No newline at end of file diff --git a/frontend/app/api/get-detectors-data/route.ts b/frontend/app/api/get-detectors-data/route.ts new file mode 100644 index 0000000..3a4a45d --- /dev/null +++ b/frontend/app/api/get-detectors-data/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server' +import { readFile } from 'fs/promises' +import { join } from 'path' + +export async function GET() { + try { + // Имитация полученных данных в формате json (данные в data/detectors.json) + const filePath = join(process.cwd(), 'frontend', 'data', 'detectors.json') + const fileContent = await readFile(filePath, 'utf8') + const allData = JSON.parse(fileContent) + + return NextResponse.json({ + success: true, + data: allData, + objectsCount: Object.keys(allData.objects || {}).length, + detectorsCount: Object.keys(allData.detectors || {}).length + }) + } catch (error) { + console.error('Error fetching detectors data:', error) + return NextResponse.json( + { + success: false, + error: 'Failed to fetch detectors data' + }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 76aba25..626ec7f 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -43,3 +43,34 @@ body { color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +/* Стилизация скроллбара */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #0e111a; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #374151; + border-radius: 4px; + border: 1px solid #1f2937; +} + +::-webkit-scrollbar-thumb:hover { + background: #4b5563; +} + +::-webkit-scrollbar-corner { + background: #0e111a; +} + +/* Стилизация скроллбара - Firefox*/ +* { + scrollbar-width: thin; + scrollbar-color: #374151 #0e111a; +} diff --git a/frontend/app/store/navigationStore.ts b/frontend/app/store/navigationStore.ts new file mode 100644 index 0000000..b48c7be --- /dev/null +++ b/frontend/app/store/navigationStore.ts @@ -0,0 +1,207 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +export interface DetectorType { + detector_id: number + name: string + object: string + status: string + 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 +} + +export interface NavigationStore { + currentObject: { id: string | undefined; title: string | undefined } + navigationHistory: string[] + currentSubmenu: string | null + + showMonitoring: boolean + showFloorNavigation: boolean + showNotifications: boolean + + selectedDetector: DetectorType | null + showDetectorMenu: boolean + selectedNotification: NotificationType | null + showNotificationDetectorInfo: boolean + + setCurrentObject: (id: string | undefined, title: string | undefined) => void + clearCurrentObject: () => void + addToHistory: (path: string) => void + goBack: () => string | null + + setCurrentSubmenu: (submenu: string | null) => void + clearSubmenu: () => void + + openMonitoring: () => void + closeMonitoring: () => void + openFloorNavigation: () => void + closeFloorNavigation: () => void + openNotifications: () => void + closeNotifications: () => void + + setSelectedDetector: (detector: DetectorType | null) => void + setShowDetectorMenu: (show: boolean) => void + setSelectedNotification: (notification: NotificationType | null) => void + setShowNotificationDetectorInfo: (show: boolean) => void + + isOnNavigationPage: () => boolean + getCurrentRoute: () => string | null + getActiveSidebarItem: () => number +} + +const useNavigationStore = create()( + persist( + (set, get) => ({ + currentObject: { + id: undefined, + title: undefined, + }, + navigationHistory: [], + currentSubmenu: null, + + showMonitoring: false, + showFloorNavigation: false, + showNotifications: false, + + selectedDetector: null, + showDetectorMenu: false, + selectedNotification: null, + showNotificationDetectorInfo: false, + + setCurrentObject: (id: string | undefined, title: string | undefined) => + set({ currentObject: { id, title } }), + + clearCurrentObject: () => + set({ currentObject: { id: undefined, title: undefined } }), + + 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 }), + + openMonitoring: () => set({ + showMonitoring: true, + showFloorNavigation: false, + showNotifications: false, + currentSubmenu: 'monitoring', + showDetectorMenu: false, + selectedDetector: null, + showNotificationDetectorInfo: false, + selectedNotification: null + }), + + closeMonitoring: () => set({ + showMonitoring: false, + currentSubmenu: null + }), + + openFloorNavigation: () => set({ + showFloorNavigation: true, + showMonitoring: false, + showNotifications: 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, + currentSubmenu: 'notifications', + showDetectorMenu: false, + selectedDetector: null + }), + + closeNotifications: () => set({ + showNotifications: false, + showNotificationDetectorInfo: false, + selectedNotification: null, + currentSubmenu: null + }), + + 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 }), + + 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 } = get() + if (showMonitoring) return 3 // Зоны Мониторинга + if (showFloorNavigation) return 4 // Навигация по этажам + if (showNotifications) return 5 // Уведомления + return 2 // Навигация (базовая) + } + }), + { + name: 'navigation-store', + } + ) +) + +export default useNavigationStore + \ No newline at end of file diff --git a/frontend/app/store/uiStore.ts b/frontend/app/store/uiStore.ts new file mode 100644 index 0000000..f70d552 --- /dev/null +++ b/frontend/app/store/uiStore.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface UIState { + isSidebarCollapsed: boolean + isNavigationSubMenuExpanded: boolean + setSidebarCollapsed: (collapsed: boolean) => void + toggleSidebar: () => void + setNavigationSubMenuExpanded: (expanded: boolean) => void + toggleNavigationSubMenu: () => void +} + +const useUIStore = create()( + persist( + (set, get) => ({ + isSidebarCollapsed: false, + isNavigationSubMenuExpanded: false, + + setSidebarCollapsed: (collapsed: boolean) => set({ isSidebarCollapsed: collapsed }), + toggleSidebar: () => set({ isSidebarCollapsed: !get().isSidebarCollapsed }), + setNavigationSubMenuExpanded: (expanded: boolean) => set({ isNavigationSubMenuExpanded: expanded }), + toggleNavigationSubMenu: () => set({ isNavigationSubMenuExpanded: !get().isNavigationSubMenuExpanded }), + }), + { + name: 'ui-store', + } + ) +) + +export default useUIStore + \ No newline at end of file diff --git a/frontend/components/alerts/DetectorList.tsx b/frontend/components/alerts/DetectorList.tsx new file mode 100644 index 0000000..0d0339f --- /dev/null +++ b/frontend/components/alerts/DetectorList.tsx @@ -0,0 +1,246 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import detectorsData from '../../data/detectors.json' + +interface Detector { + detector_id: number + name: string + location: string + status: string + object: string + floor: number + checked: boolean +} + +// Interface for raw detector data from JSON +interface RawDetector { + detector_id: number + name: string + object: string + status: string + type: string + location: string + floor: number + notifications: Array<{ + id: number + type: string + message: string + timestamp: string + acknowledged: boolean + priority: string + }> +} + +type FilterType = 'all' | 'critical' | 'warning' | 'normal' + +interface DetectorListProps { + objectId?: string + selectedDetectors: number[] + onDetectorSelect: (detectorId: number, selected: boolean) => void +} + +const DetectorList: React.FC = ({ objectId, selectedDetectors, onDetectorSelect }) => { + const [detectors, setDetectors] = useState([]) + const [selectedFilter, setSelectedFilter] = useState('all') + const [searchTerm, setSearchTerm] = useState('') + + useEffect(() => { + const detectorsArray = Object.values(detectorsData.detectors).filter( + (detector: RawDetector) => objectId ? detector.object === objectId : true + ) + setDetectors(detectorsArray as Detector[]) + + + }, [objectId]) + + + + const filteredDetectors = detectors.filter(detector => { + const matchesSearch = detector.name.toLowerCase().includes(searchTerm.toLowerCase()) || + detector.location.toLowerCase().includes(searchTerm.toLowerCase()) + + if (selectedFilter === 'all') return matchesSearch + if (selectedFilter === 'critical') return matchesSearch && detector.status === '#b3261e' + if (selectedFilter === 'warning') return matchesSearch && detector.status === '#fd7c22' + if (selectedFilter === 'normal') return matchesSearch && detector.status === '#00ff00' + + return matchesSearch + }) + + + + return ( +
+
+
+ + + + +
+ +
+
+ setSearchTerm(e.target.value)} + className="bg-[#161824] text-white placeholder-gray-400 px-4 py-2 rounded-lg border border-gray-600 focus:border-blue-500 focus:outline-none w-64" + /> + + + +
+
+
+ + {/* Таблица детекторов */} +
+
+ + + + + + + + + + + + {filteredDetectors.map((detector) => { + const isSelected = selectedDetectors.includes(detector.detector_id) + + return ( + + + + + + + + ) + })} + +
+ 0} + onChange={(e) => { + if (e.target.checked) { + filteredDetectors.forEach(detector => { + if (!selectedDetectors.includes(detector.detector_id)) { + onDetectorSelect(detector.detector_id, true) + } + }) + } else { + filteredDetectors.forEach(detector => { + if (selectedDetectors.includes(detector.detector_id)) { + onDetectorSelect(detector.detector_id, false) + } + }) + } + }} + className="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2" + /> + ДетекторСтатусМестоположениеПроверен
+ onDetectorSelect(detector.detector_id, e.target.checked)} + className="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded focus:ring-blue-500 focus:ring-2" + /> + {detector.name} +
+
+ + {detector.status === '#b3261e' ? 'Критическое' : + detector.status === '#fd7c22' ? 'Предупреждение' : 'Норма'} + +
+
{detector.location} + {detector.checked ? ( +
+ + + + Да +
+ ) : ( + Нет + )} +
+
+
+ + {/* Статы детекторров*/} +
+
+
{filteredDetectors.length}
+
Всего
+
+
+
{filteredDetectors.filter(d => d.status === '#00ff00').length}
+
Норма
+
+
+
{filteredDetectors.filter(d => d.status === '#fd7c22').length}
+
Предупреждения
+
+
+
{filteredDetectors.filter(d => d.status === '#b3261e').length}
+
Критические
+
+
+ + {filteredDetectors.length === 0 && ( +
+

Детекторы не найдены

+
+ )} +
+ ) +} + +export default DetectorList \ No newline at end of file diff --git a/frontend/components/dashboard/AreaChart.tsx b/frontend/components/dashboard/AreaChart.tsx new file mode 100644 index 0000000..61c20bd --- /dev/null +++ b/frontend/components/dashboard/AreaChart.tsx @@ -0,0 +1,48 @@ +'use client' + +import React from 'react' + +interface ChartDataPoint { + value: number + label?: string + timestamp?: string +} + +interface AreaChartProps { + className?: string + data?: ChartDataPoint[] +} + +const AreaChart: React.FC = ({ className = '' }) => { + return ( +
+ + + + + + + + + + + + + + + + + +
+ ) +} + +export default AreaChart \ No newline at end of file diff --git a/frontend/components/dashboard/BarChart.tsx b/frontend/components/dashboard/BarChart.tsx new file mode 100644 index 0000000..7485ad1 --- /dev/null +++ b/frontend/components/dashboard/BarChart.tsx @@ -0,0 +1,62 @@ +'use client' + +import React from 'react' + +interface ChartDataPoint { + value: number + label?: string + color?: string +} + +interface BarChartProps { + className?: string + data?: ChartDataPoint[] +} + +const BarChart: React.FC = ({ className = '' }) => { + const barData = [ + { value: 80, color: 'rgb(42, 157, 144)' }, + { value: 65, color: 'rgb(42, 157, 144)' }, + { value: 90, color: 'rgb(42, 157, 144)' }, + { value: 45, color: 'rgb(42, 157, 144)' }, + { value: 75, color: 'rgb(42, 157, 144)' }, + { value: 55, color: 'rgb(42, 157, 144)' }, + { value: 85, color: 'rgb(42, 157, 144)' }, + { value: 70, color: 'rgb(42, 157, 144)' }, + { value: 60, color: 'rgb(42, 157, 144)' }, + { value: 95, color: 'rgb(42, 157, 144)' }, + { value: 40, color: 'rgb(42, 157, 144)' }, + { value: 80, color: 'rgb(42, 157, 144)' } + ] + + return ( +
+ + + {barData.map((bar, index) => { + const barWidth = 40 + const barSpacing = 12 + const x = index * (barWidth + barSpacing) + 20 + const barHeight = (bar.value / 100) * 160 + const y = 180 - barHeight + + return ( + + ) + })} + + +
+ ) +} + +export default BarChart \ No newline at end of file diff --git a/frontend/components/dashboard/ChartCard.tsx b/frontend/components/dashboard/ChartCard.tsx new file mode 100644 index 0000000..7e556af --- /dev/null +++ b/frontend/components/dashboard/ChartCard.tsx @@ -0,0 +1,41 @@ +'use client' + +import React from 'react' + +interface ChartCardProps { + title: string + subtitle?: string + children: React.ReactNode + className?: string +} + +const ChartCard: React.FC = ({ + title, + subtitle, + children, + className = '' +}) => { + return ( +
+
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+
+ + + +
+
+ +
+ {children} +
+
+ ) +} + +export default ChartCard \ No newline at end of file diff --git a/frontend/components/dashboard/Dashboard.tsx b/frontend/components/dashboard/Dashboard.tsx new file mode 100644 index 0000000..475a54d --- /dev/null +++ b/frontend/components/dashboard/Dashboard.tsx @@ -0,0 +1,263 @@ +'use client' + +import React from 'react' +import { useRouter } from 'next/navigation' +import Sidebar from '../ui/Sidebar' +import useNavigationStore from '../../app/store/navigationStore' +import ChartCard from './ChartCard' +import AreaChart from './AreaChart' +import BarChart from './BarChart' +import DetectorChart from './DetectorChart' +import detectorsData from '../../data/detectors.json' + +const Dashboard: React.FC = () => { + const router = useRouter() + const { currentObject, setCurrentSubmenu, closeMonitoring, closeFloorNavigation, closeNotifications } = useNavigationStore() + const objectId = currentObject?.id + const objectTitle = currentObject?.title + + const handleBackClick = () => { + router.push('/objects') + } + + interface DetectorData { + detector_id: number + name: string + object: string + status: string + type: string + location: string + floor: number + checked?: boolean + notifications: Array<{ + id: number + type: string + message: string + timestamp: string + acknowledged: boolean + priority: string + }> + } + + const detectorsArray = Object.values(detectorsData.detectors).filter( + (detector: DetectorData) => objectId ? detector.object === objectId : true + ) + + // Статусы + const statusCounts = detectorsArray.reduce((acc: { critical: number; warning: number; normal: number }, detector: DetectorData) => { + if (detector.status === '#b3261e') acc.critical++ + else if (detector.status === '#fd7c22') acc.warning++ + else if (detector.status === '#00ff00') acc.normal++ + return acc + }, { critical: 0, warning: 0, normal: 0 }) + + const handleNavigationClick = () => { + // Close all submenus before navigating + closeMonitoring() + closeFloorNavigation() + closeNotifications() + setCurrentSubmenu(null) + router.push('/navigation') + } + + // No custom sidebar handling needed - using unified navigation service + + return ( +
+ + +
+
+
+ + +
+
+ +
+
+

Объект {objectId?.replace('object_', '')}

+ +
+ + +
+ + +
+ + + + Период +
+
+
+
+ + {/* Карты-графики */} +
+ + + + + + + +
+
+ + {/* Список детекторов */} +
+
+
+

Тренды

+
+ + + + Месяц + + + +
+
+ + {/* Таблица */} +
+
+ + + + + + + + + + + {detectorsArray.map((detector: DetectorData) => ( + + + + + + + ))} + +
ДетекторСтатусМестоположениеПроверен
{detector.name} +
+
+ + {detector.status === '#b3261e' ? 'Критическое' : + detector.status === '#fd7c22' ? 'Предупреждение' : 'Норма'} + +
+
{detector.location} + {detector.checked ? ( +
+ + + + Да +
+ ) : ( + Нет + )} +
+
+ + {/* Статы */} +
+
+
{detectorsArray.length}
+
Всего
+
+
+
{statusCounts.normal}
+
Норма
+
+
+
{statusCounts.warning}
+
Предупреждения
+
+
+
{statusCounts.critical}
+
Критические
+
+
+
+ + {/* Графики с аналитикой */} +
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+ ) +} + +export default Dashboard \ No newline at end of file diff --git a/frontend/components/dashboard/DetectorChart.tsx b/frontend/components/dashboard/DetectorChart.tsx new file mode 100644 index 0000000..19b5953 --- /dev/null +++ b/frontend/components/dashboard/DetectorChart.tsx @@ -0,0 +1,103 @@ +'use client' + +import React from 'react' + +interface DetectorDataPoint { + value: number + label?: string + timestamp?: string + status?: string +} + +interface DetectorChartProps { + className?: string + data?: DetectorDataPoint[] + type?: 'line' | 'bar' +} + +const DetectorChart: React.FC = ({ + className = '', + type = 'line' +}) => { + if (type === 'bar') { + const barData = [ + { value: 85, label: 'Янв' }, + { value: 70, label: 'Фев' }, + { value: 90, label: 'Мар' }, + { value: 65, label: 'Апр' }, + { value: 80, label: 'Май' }, + { value: 95, label: 'Июн' } + ] + + return ( +
+ + + {barData.map((bar, index) => { + const barWidth = 50 + const barSpacing = 15 + const x = index * (barWidth + barSpacing) + 20 + const barHeight = (bar.value / 100) * 150 + const y = 160 - barHeight + + return ( + + + + {bar.label} + + + ) + })} + + +
+ ) + } + + return ( +
+ + + + + + + + + + + + + + + + + +
+ ) +} + +export default DetectorChart \ No newline at end of file diff --git a/frontend/components/ModelViewer.tsx b/frontend/components/model/ModelViewer.tsx similarity index 72% rename from frontend/components/ModelViewer.tsx rename to frontend/components/model/ModelViewer.tsx index 4a4f04c..b563a6d 100644 --- a/frontend/components/ModelViewer.tsx +++ b/frontend/components/model/ModelViewer.tsx @@ -1,23 +1,21 @@ 'use client' -import React, { useEffect, useRef, useState, useCallback } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Engine, Scene, Vector3, HemisphericLight, ArcRotateCamera, - MeshBuilder, - StandardMaterial, Color3, Color4, AbstractMesh, - Mesh, Nullable, SceneLoader } from '@babylonjs/core' import '@babylonjs/loaders' -import { getCacheFileName, loadCachedData, parseAndCacheScene, ParsedMeshData } from '../utils/meshCache' +import LoadingSpinner from '../ui/LoadingSpinner' + interface ModelViewerProps { modelPath: string @@ -40,6 +38,8 @@ const ModelViewer: React.FC = ({ const engineRef = useRef>(null) const sceneRef = useRef>(null) const [isLoading, setIsLoading] = useState(false) + const [loadingProgress, setLoadingProgress] = useState(0) + const [showModel, setShowModel] = useState(false) const isInitializedRef = useRef(false) const isDisposedRef = useRef(false) @@ -127,18 +127,22 @@ const ModelViewer: React.FC = ({ } setIsLoading(true) - console.log('🚀 Loading GLTF model:', modelPath) + setLoadingProgress(0) + setShowModel(false) + console.log('Loading GLTF model:', modelPath) + + // UI элемент загрузчика (есть эффект замедленности) + const progressInterval = setInterval(() => { + setLoadingProgress(prev => { + if (prev >= 90) { + clearInterval(progressInterval) + return 90 + } + return prev + Math.random() * 15 + }) + }, 100) try { - const cacheFileName = getCacheFileName(modelPath) - const cachedData = await loadCachedData(cacheFileName) - - if (cachedData) { - console.log('📦 Using cached mesh data for analysis') - } else { - console.log('🔄 No cached data found, parsing scene...') - } - const result = await SceneLoader.ImportMeshAsync( '', modelPath, @@ -146,9 +150,11 @@ const ModelViewer: React.FC = ({ sceneRef.current ) - console.log('✅ GLTF Model loaded successfully!') - console.log('📊 Loaded Meshes:', result.meshes.length) + clearInterval(progressInterval) + setLoadingProgress(100) + console.log('GLTF Model loaded successfully!') + if (result.meshes.length > 0) { const boundingBox = result.meshes[0].getHierarchyBoundingVectors() @@ -159,8 +165,7 @@ const ModelViewer: React.FC = ({ camera.radius = maxDimension * 2 camera.target = result.meshes[0].position - const parsedData = await parseAndCacheScene(result.meshes, cacheFileName, modelPath) - + onModelLoaded?.({ meshes: result.meshes, boundingBox: { @@ -169,38 +174,48 @@ const ModelViewer: React.FC = ({ } }) - console.log('🎉 Model ready for viewing!') + // Плавное появление модели + setTimeout(() => { + if (!isDisposedRef.current) { + setShowModel(true) + setIsLoading(false) + } + }, 500) } else { - console.warn('⚠️ No meshes found in model') + console.warn('No meshes found in model') onError?.('No geometry found in model') - } - } catch (error) { - console.error('❌ Error loading GLTF model:', error) - onError?.(`Failed to load model: ${error}`) - } finally { - if (!isDisposedRef.current) { setIsLoading(false) } + } catch (error) { + clearInterval(progressInterval) + console.error('Error loading GLTF model:', error) + onError?.(`Failed to load model: ${error}`) + setIsLoading(false) } } loadModel() - }, [modelPath]) + }, [modelPath, onError, onModelLoaded]) return (
{isLoading && ( -
-
- Loading Model... +
+
)}
) } -export default ModelViewer \ No newline at end of file +export default ModelViewer \ No newline at end of file diff --git a/frontend/components/navigation/DetectorMenu.tsx b/frontend/components/navigation/DetectorMenu.tsx new file mode 100644 index 0000000..6f892ac --- /dev/null +++ b/frontend/components/navigation/DetectorMenu.tsx @@ -0,0 +1,97 @@ +'use client' + +import React from 'react' + +interface DetectorType { + detector_id: number + name: string + object: string + status: string + checked: boolean + type: string + location: string + floor: number +} + +interface DetectorMenuProps { + detector: DetectorType + isOpen: boolean + onClose: () => void + getStatusText: (status: string) => string +} + +const DetectorMenu: React.FC = ({ detector, isOpen, onClose, getStatusText }) => { + if (!isOpen) return null + + return ( +
+
+ {/* Header */} +
+

+ Датч.{detector.name} +

+
+ + +
+
+ + {/* Detector Information Table */} +
+
+
+
Маркировка по проекту
+
{detector.name}
+
+
+
Тип детектора
+
{detector.type}
+
+
+
+
+
Местоположение
+
{detector.location}
+
+
+
Статус
+
{getStatusText(detector.status)}
+
+
+
+
+
Временная метка
+
Сегодня, 14:30
+
+
+
Вчера
+
+
+
+ + {/* Close Button */} + +
+
+ ) +} + +export default DetectorMenu \ No newline at end of file diff --git a/frontend/components/navigation/FloorNavigation.tsx b/frontend/components/navigation/FloorNavigation.tsx new file mode 100644 index 0000000..cc5b2c4 --- /dev/null +++ b/frontend/components/navigation/FloorNavigation.tsx @@ -0,0 +1,206 @@ +'use client' + +import React, { useState } from 'react' + +interface DetectorsDataType { + detectors: Record +} + +interface FloorNavigationProps { + objectId?: string + detectorsData: DetectorsDataType + onDetectorMenuClick: (detector: DetectorType) => void + onClose?: () => void +} + +interface DetectorType { + detector_id: number + name: string + object: string + status: string + checked: boolean + type: string + location: string + floor: number + notifications: Array<{ + id: number + type: string + message: string + timestamp: string + acknowledged: boolean + priority: string + }> +} + +const FloorNavigation: React.FC = ({ objectId, detectorsData, onDetectorMenuClick, onClose }) => { + const [expandedFloors, setExpandedFloors] = useState>(new Set()) + 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()) + ) + } + + // Группиовка детекторов по этажам + const detectorsByFloor = filteredDetectors.reduce((acc, detector) => { + const floor = detector.floor + if (!acc[floor]) { + acc[floor] = [] + } + acc[floor].push(detector) + return acc + }, {} as Record) + + // Сортировка этажей + const sortedFloors = Object.keys(detectorsByFloor) + .map(Number) + .sort((a, b) => a - b) + + const toggleFloor = (floor: number) => { + const newExpanded = new Set(expandedFloors) + if (newExpanded.has(floor)) { + newExpanded.delete(floor) + } else { + newExpanded.add(floor) + } + setExpandedFloors(newExpanded) + } + + 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) => { + onDetectorMenuClick(detector) + } + + return ( +
+
+
+

Навигация по этажам

+ {onClose && ( + + )} +
+ +
+
+ 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" + /> + + + +
+ +
+ +
+ {sortedFloors.map(floor => { + const floorDetectors = detectorsByFloor[floor] + const isExpanded = expandedFloors.has(floor) + + return ( +
+ + + {/* суб-меню с детекторами */} + {isExpanded && ( +
+ {floorDetectors.map(detector => ( +
+
+
{detector.name}
+
{detector.location}
+
+
+
+ {getStatusText(detector.status)} + {detector.checked && ( + + + + )} + +
+
+ ))} +
+ )} +
+ ) + })} +
+
+ +
+ ) +} + +export default FloorNavigation \ No newline at end of file diff --git a/frontend/components/navigation/Monitoring.tsx b/frontend/components/navigation/Monitoring.tsx new file mode 100644 index 0000000..c669d54 --- /dev/null +++ b/frontend/components/navigation/Monitoring.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import Image from 'next/image'; + +interface MonitoringProps { + objectId?: string; + onClose?: () => void; +} + +const Monitoring: React.FC = ({ onClose }) => { + return ( +
+
+
+

Зоны мониторинга

+ {onClose && ( + + )} +
+ +
+
+ Object Model { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + }} + /> +
+
+ +
+ {[1, 2, 3, 4, 5, 6].map((zone) => ( +
+
+ {`Зона { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + }} + /> +
+
+ ))} +
+
+
+ ); +}; + +export default Monitoring; \ No newline at end of file diff --git a/frontend/components/notifications/NotificationDetectorInfo.tsx b/frontend/components/notifications/NotificationDetectorInfo.tsx new file mode 100644 index 0000000..c87a60f --- /dev/null +++ b/frontend/components/notifications/NotificationDetectorInfo.tsx @@ -0,0 +1,175 @@ +'use client' + +import React from 'react' + +interface DetectorInfoType { + detector_id: number + name: string + object: string + status: string + type: string + location: string + floor: number + checked: boolean + notifications: Array<{ + id: number + type: string + message: string + timestamp: string + acknowledged: boolean + priority: string + }> +} + +interface NotificationDetectorInfoProps { + detectorData: DetectorInfoType + onClose: () => void +} + +const NotificationDetectorInfo: React.FC = ({ detectorData, onClose }) => { + const detectorInfo = detectorData + + if (!detectorInfo) { + return ( +
+
+
+

Информация о детекторе

+ +
+

Детектор не найден

+
+
+ ) + } + + const getStatusColor = (status: string) => { + if (status === '#b3261e') return 'text-red-400' + if (status === '#fd7c22') return 'text-orange-400' + if (status === '#4caf50') return 'text-green-400' + return 'text-gray-400' + } + + 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' + } + } + + // Get the latest notification for this detector + const latestNotification = detectorInfo.notifications && detectorInfo.notifications.length > 0 + ? detectorInfo.notifications.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0] + : 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' + }) + } + + return ( +
+
+
+

+ Датч.{detectorInfo.name} +

+
+ + +
+
+ + {/* Табличка с детекторами */} +
+
+
+
Маркировка по проекту
+
{detectorInfo.name}
+
+
+
Тип детектора
+
{detectorInfo.type}
+
+
+
+
+
Местоположение
+
{detectorInfo.location}
+
+
+
Статус
+
+
+ + {detectorInfo.status === '#b3261e' ? 'Критический' : + detectorInfo.status === '#fd7c22' ? 'Предупреждение' : + detectorInfo.status === '#4caf50' ? 'Нормальный' : 'Неизвестно'} + +
+
+
+ {latestNotification && ( +
+
+
Последнее уведомление
+
{formatDate(latestNotification.timestamp)}
+
+
+
Приоритет
+
+ {latestNotification.priority} +
+
+
+ )} + {latestNotification && ( +
+
Сообщение
+
{latestNotification.message}
+
+ )} +
+ + +
+
+ ) +} + +export default NotificationDetectorInfo \ No newline at end of file diff --git a/frontend/components/notifications/Notifications.tsx b/frontend/components/notifications/Notifications.tsx new file mode 100644 index 0000000..b4adbc7 --- /dev/null +++ b/frontend/components/notifications/Notifications.tsx @@ -0,0 +1,134 @@ +'use client' + +import React from 'react' +import { IoClose } from 'react-icons/io5' + +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 DetectorType { + detector_id: number + name: string + object: string + status: string + 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 +} + +interface NotificationsProps { + objectId?: string + detectorsData: DetectorsDataType + onNotificationClick: (notification: NotificationType) => void + onClose: () => void +} + +const Notifications: React.FC = ({ objectId, detectorsData, onNotificationClick, onClose }) => { + const allNotifications = React.useMemo(() => { + const notifications: NotificationType[] = []; + 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 + }); + }); + } + }); + return notifications; + }, [detectorsData]); + + // сортировка по objectId + const filteredNotifications = objectId + ? allNotifications.filter(notification => notification.object.toString() === objectId.toString()) + : allNotifications + + // сортировка по timestamp + const sortedNotifications = [...filteredNotifications].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + + 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 ( +
+
+

Уведомления

+ +
+ + {/* Список уведомлений*/} +
+ {sortedNotifications.length === 0 ? ( +
+

Уведомления не найдены

+
+ ) : ( +
+ {sortedNotifications.map((notification) => ( +
+
+
+
+
{notification.detector_name}
+
{notification.message}
+
+
+ +
+ ))} +
+ )} +
+
+ ) +} + +export default Notifications \ No newline at end of file diff --git a/frontend/components/objects/ObjectCard.tsx b/frontend/components/objects/ObjectCard.tsx new file mode 100644 index 0000000..875a5fc --- /dev/null +++ b/frontend/components/objects/ObjectCard.tsx @@ -0,0 +1,103 @@ +'use client' + +import React from 'react' +import Image from 'next/image' +import { useNavigationService } from '@/services/navigationService' +interface ObjectData { + object_id: string + title: string + description: string + image: string + location: string + floors?: number + area?: string + type?: string + status?: string +} + +interface ObjectCardProps { + object: ObjectData + onSelect?: (objectId: string) => void + isSelected?: boolean +} + +// Иконка редактирования +const EditIcon = ({ className }: { className?: string }) => ( + + + +) + +const ObjectCard: React.FC = ({ object, onSelect, isSelected = false }) => { + const navigationService = useNavigationService() + + const handleCardClick = () => { + if (onSelect) { + onSelect(object.object_id) + } + // Навигация к дашборду с выбранным объектом + navigationService.selectObjectAndGoToDashboard(object.object_id, object.title) + } + + const handleEditClick = (e: React.MouseEvent) => { + e.stopPropagation() + console.log('Edit object:', object.object_id) + // Логика редактирования объекта + } + + return ( +
{ + if (e.key === 'Enter' || e.key === ' ') { + handleCardClick() + } + }} + > +
+
+

+ {object.title} +

+

+ {object.description} +

+
+ +
+ + {/* Изображение объекта */} +
+ {object.title} { + // Заглушка при ошибке загрузки изображения + const target = e.target as HTMLImageElement + target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDUyIiBoZWlnaHQ9IjMwMiIgdmlld0JveD0iMCAwIDQ1MiAzMDIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0NTIiIGhlaWdodD0iMzAyIiBmaWxsPSIjRjFGMUYxIi8+CjxwYXRoIGQ9Ik0yMjYgMTUxTDI0NiAxMzFMMjY2IDE1MUwyNDYgMTcxTDIyNiAxNTFaIiBmaWxsPSIjOTk5OTk5Ii8+Cjx0ZXh0IHg9IjIyNiIgeT0iMTkwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjOTk5OTk5IiBmb250LXNpemU9IjE0Ij7QntCx0YrQtdC60YI8L3RleHQ+Cjwvc3ZnPgo=' + }} + /> +
+
+ ) +} + +export default ObjectCard +export type { ObjectData, ObjectCardProps } \ No newline at end of file diff --git a/frontend/components/objects/ObjectGallery.tsx b/frontend/components/objects/ObjectGallery.tsx new file mode 100644 index 0000000..3cfbc2d --- /dev/null +++ b/frontend/components/objects/ObjectGallery.tsx @@ -0,0 +1,102 @@ +'use client' + +import React from 'react' +import ObjectCard, { ObjectData } from './ObjectCard' + +interface ObjectGalleryProps { + objects: ObjectData[] + title?: string + onObjectSelect?: (objectId: string) => void + selectedObjectId?: string | null + className?: string +} + +const BackIcon = ({ className }: { className?: string }) => ( + + + +) + +const ObjectGallery: React.FC = ({ + objects, + title = 'Объекты', + onObjectSelect, + selectedObjectId, + className = '' +}) => { + + const handleObjectSelect = (objectId: string) => { + if (onObjectSelect) { + onObjectSelect(objectId) + } + } + + const handleBackClick = () => { + console.log('Back clicked') + } + + return ( +
+
+
+
+ + +
+

+ {title} +

+
+ + +
+ + {/* Галерея объектов */} + {objects.length > 0 ? ( +
+ {objects.map((object) => ( + + ))} +
+ ) : ( +
+
+
+ + + +
+

Объекты не найдены

+

Нет доступных объектов

+
+
+ )} +
+
+
+ ) +} + +export default ObjectGallery +export type { ObjectGalleryProps } \ No newline at end of file diff --git a/frontend/components/reports/ReportsList.tsx b/frontend/components/reports/ReportsList.tsx new file mode 100644 index 0000000..a99f63c --- /dev/null +++ b/frontend/components/reports/ReportsList.tsx @@ -0,0 +1,288 @@ +'use client'; + +import React, { useState, useMemo } from 'react'; + +interface NotificationType { + id: number; + type: string; + message: string; + timestamp: string; + acknowledged: boolean; + priority: string; + detector_id: number; + detector_name: string; + location: string; + object: string; +} + +interface DetectorType { + detector_id: number + name: string + object: string + status: string + 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 +} + +interface ReportsListProps { + objectId?: string; + detectorsData: DetectorsDataType; +} + +const ReportsList: React.FC = ({ detectorsData }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [priorityFilter] = useState('all'); + const [acknowledgedFilter] = useState('all'); + + const allNotifications = useMemo(() => { + const notifications: NotificationType[] = []; + 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 + }); + }); + } + }); + return notifications; + }, [detectorsData]); + + const filteredDetectors = useMemo(() => { + return allNotifications.filter(notification => { + const matchesSearch = notification.detector_name.toLowerCase().includes(searchTerm.toLowerCase()) || + notification.location.toLowerCase().includes(searchTerm.toLowerCase()) || + notification.message.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = statusFilter === 'all' || notification.type === statusFilter; + const matchesPriority = priorityFilter === 'all' || notification.priority === priorityFilter; + const matchesAcknowledged = acknowledgedFilter === 'all' || + (acknowledgedFilter === 'acknowledged' && notification.acknowledged) || + (acknowledgedFilter === 'unacknowledged' && !notification.acknowledged); + + return matchesSearch && matchesStatus && matchesPriority && matchesAcknowledged; + }); + }, [allNotifications, searchTerm, statusFilter, priorityFilter, acknowledgedFilter]); + + const getStatusColor = (type: string) => { + switch (type) { + case 'critical': return '#b3261e'; + case 'warning': return '#fd7c22'; + case 'info': return '#00ff00'; + default: return '#666'; + } + }; + + const getPriorityColor = (priority: string) => { + switch (priority) { + case 'high': return '#b3261e'; + case 'medium': return '#fd7c22'; + case 'low': return '#00ff00'; + default: return '#666'; + } + }; + + const getStatusCounts = () => { + const counts = { + total: allNotifications.length, + critical: allNotifications.filter(d => d.type === 'critical').length, + warning: allNotifications.filter(d => d.type === 'warning').length, + info: allNotifications.filter(d => d.type === 'info').length, + acknowledged: allNotifications.filter(d => d.acknowledged).length, + unacknowledged: allNotifications.filter(d => !d.acknowledged).length + }; + return counts; + }; + + const counts = getStatusCounts(); + + return ( +
+ {/* Поиск и сортировка*/} +
+
+ + + + +
+ +
+
+ setSearchTerm(e.target.value)} + className="bg-[#161824] text-white placeholder-gray-400 px-4 py-2 rounded-lg border border-gray-600 focus:border-blue-500 focus:outline-none w-64" + /> + + + +
+
+
+ + {/* Табличка с детекторами*/} +
+
+ + + + + + + + + + + + + + {filteredDetectors.map((detector) => ( + + + + + + + + + + ))} + {filteredDetectors.length === 0 && ( + + + + )} + +
ДетекторСтатусСообщениеМестоположениеПриоритетПодтвержденоВремя
+
{detector.detector_name}
+
ID: {detector.detector_id}
+
+
+
+ + {detector.type === 'critical' ? 'Критический' : + detector.type === 'warning' ? 'Предупреждение' : 'Информация'} + +
+
+
{detector.message}
+
+
{detector.location}
+
+ + {detector.priority === 'high' ? 'Высокий' : + detector.priority === 'medium' ? 'Средний' : 'Низкий'} + + + + {detector.acknowledged ? 'Да' : 'Нет'} + + +
+ {new Date(detector.timestamp).toLocaleString('ru-RU')} +
+
+ Детекторы не найдены +
+
+
+ +
+
+
{counts.total}
+
Всего
+
+
+
{counts.critical}
+
Критические
+
+
+
{counts.warning}
+
Предупреждения
+
+
+
{counts.info}
+
Информация
+
+
+
{counts.acknowledged}
+
Подтверждено
+
+
+
{counts.unacknowledged}
+
Не подтверждено
+
+
+
+ ); +}; + +export default ReportsList; \ No newline at end of file diff --git a/frontend/components/ui/ExportMenu.tsx b/frontend/components/ui/ExportMenu.tsx new file mode 100644 index 0000000..ee7e3ae --- /dev/null +++ b/frontend/components/ui/ExportMenu.tsx @@ -0,0 +1,54 @@ +'use client' + +import React, { useState } from 'react' + +interface ExportMenuProps { + onExport: (format: 'csv' | 'pdf') => void +} + +const ExportMenu: React.FC = ({ onExport }) => { + const [selectedFormat, setSelectedFormat] = useState<'csv' | 'pdf'>('csv') + + const handleExport = () => { + onExport(selectedFormat) + } + + return ( +
+
+ + + +
+ + +
+ ) +} + +export default ExportMenu \ No newline at end of file diff --git a/frontend/components/ui/LoadingSpinner.tsx b/frontend/components/ui/LoadingSpinner.tsx new file mode 100644 index 0000000..856cca6 --- /dev/null +++ b/frontend/components/ui/LoadingSpinner.tsx @@ -0,0 +1,72 @@ +'use client' + +import React from 'react' + +interface LoadingSpinnerProps { + progress?: number // 0-100 + size?: number // diameter in pixels + strokeWidth?: number + className?: string +} + +const LoadingSpinner: React.FC = ({ + progress = 0, + size = 120, + strokeWidth = 8, + className = '' +}) => { + const radius = (size - strokeWidth) / 2 + const circumference = radius * 2 * Math.PI + const strokeDasharray = circumference + const strokeDashoffset = circumference - (progress / 100) * circumference + + return ( +
+
+ {/* Background circle */} + + + {/* Progress circle */} + + + + {/* Percentage text */} +
+ + {Math.round(progress)}% + +
+
+ + {/* Loading text */} +
+ Loading Model... +
+
+ ) +} + +export default LoadingSpinner \ No newline at end of file diff --git a/frontend/components/ui/Sidebar.tsx b/frontend/components/ui/Sidebar.tsx new file mode 100644 index 0000000..7f6e782 --- /dev/null +++ b/frontend/components/ui/Sidebar.tsx @@ -0,0 +1,474 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useRouter, usePathname } from 'next/navigation' +import Image from 'next/image' +import useUIStore from '../../app/store/uiStore' +import useNavigationStore from '../../app/store/navigationStore' +import { useNavigationService } from '@/services/navigationService' + +interface NavigationItem { + id: number + label: string + icon: React.ComponentType<{ className?: string }> +} + +interface SidebarProps { + navigationItems?: NavigationItem[] + logoSrc?: string + userInfo?: { + name: string + role: string + avatar?: string + } + activeItem?: number | null + onCustomItemClick?: (itemId: number) => boolean +} + +// Иконки-заглушки +const BookOpen = ({ className }: { className?: string }) => ( + + + +) + +const Bot = ({ className }: { className?: string }) => ( + + + +) + +const SquareTerminal = ({ className }: { className?: string }) => ( + + + +) + +const CircleDot = ({ className }: { className?: string }) => ( + + + + +) + +const BellDot = ({ className }: { className?: string }) => ( + + + +) + +const History = ({ className }: { className?: string }) => ( + + + +) + +const Settings2 = ({ className }: { className?: string }) => ( + + + +) + +const Monitor = ({ className }: { className?: string }) => ( + + + + + +) + +const Building = ({ className }: { className?: string }) => ( + + + + + + + +) + +// основные routes +const mainNavigationItems: NavigationItem[] = [ + { + id: 1, + icon: BookOpen, + label: 'Дашборд' + }, + { + id: 2, + icon: Bot, + label: 'Навигация по зданию' + }, + { + id: 8, + icon: History, + label: 'История тревог' + }, + { + id: 9, + icon: Settings2, + label: 'Отчеты' + } +] + +// суб-меню под "Навигация по зданию" +const navigationSubItems: NavigationItem[] = [ + { + id: 3, + icon: Monitor, + label: 'Зоны Мониторинга' + }, + { + id: 4, + icon: Building, + label: 'Навигация по этажам' + }, + { + id: 5, + icon: BellDot, + label: 'Уведомления' + }, + { + id: 6, + icon: CircleDot, + label: 'Сенсоры' + }, + { + id: 7, + icon: SquareTerminal, + label: 'Список датчиков' + } +] + + +const Sidebar: React.FC = ({ + logoSrc, + userInfo = { + name: 'Александр', + role: 'Администратор' + }, + activeItem: propActiveItem, + onCustomItemClick +}) => { + const navigationService = useNavigationService() + const router = useRouter() + const pathname = usePathname() + const [internalActiveItem, setInternalActiveItem] = useState(null) + const [isHydrated, setIsHydrated] = useState(false) + const [manuallyToggled, setManuallyToggled] = useState(false) + const activeItem = propActiveItem !== undefined ? propActiveItem : internalActiveItem + const { + isSidebarCollapsed: isCollapsed, + toggleSidebar, + isNavigationSubMenuExpanded: showNavigationSubItems, + setNavigationSubMenuExpanded: setShowNavigationSubItems, + toggleNavigationSubMenu + } = useUIStore() + + const { + openMonitoring, + openFloorNavigation, + openNotifications, + closeMonitoring, + closeFloorNavigation, + closeNotifications, + showMonitoring, + showFloorNavigation, + showNotifications + } = useNavigationStore() + + useEffect(() => { + setIsHydrated(true) + }, []) + + // Чек если суб-меню активны + const isNavigationSubItemActive = activeItem && [3, 4, 5, 6, 7].includes(activeItem) + const shouldShowNavigationAsActive = activeItem === 2 || isNavigationSubItemActive + + // Авто-расткрытие меню, если суб-меню стало активным (только если не было ручного переключения) + useEffect(() => { + if (isNavigationSubItemActive && !showNavigationSubItems && !manuallyToggled) { + setShowNavigationSubItems(true) + } + }, [isNavigationSubItemActive, showNavigationSubItems, manuallyToggled, setShowNavigationSubItems]) + + const handleItemClick = (itemId: number) => { + let handled = false + + // Handle submenu items directly with navigation store + switch (itemId) { + case 2: // Navigation - only navigate to page, don't open submenus + if (pathname !== '/navigation') { + router.push('/navigation') + } + handled = true + break + case 3: // Monitoring + if (pathname !== '/navigation') { + // Navigate to navigation page first, then open monitoring + router.push('/navigation') + setTimeout(() => openMonitoring(), 100) + } else if (showMonitoring) { + // Close if already open + closeMonitoring() + } else { + openMonitoring() + } + handled = true + break + case 4: // Floor Navigation + if (pathname !== '/navigation') { + // Navigate to navigation page first, then open floor navigation + router.push('/navigation') + setTimeout(() => openFloorNavigation(), 100) + } else if (showFloorNavigation) { + // Close if already open + closeFloorNavigation() + } else { + openFloorNavigation() + } + handled = true + break + case 5: // Notifications + if (pathname !== '/navigation') { + // Navigate to navigation page first, then open notifications + router.push('/navigation') + setTimeout(() => openNotifications(), 100) + } else if (showNotifications) { + // Close if already open + closeNotifications() + } else { + openNotifications() + } + handled = true + break + default: + // For other items, use navigation service + if (navigationService) { + handled = navigationService.handleSidebarItemClick(itemId, pathname) + } + break + } + + if (handled) { + // Update internal active item state + if (propActiveItem === undefined) { + setInternalActiveItem(itemId) + } + + // Call custom handler if provided (for additional logic) + if (onCustomItemClick) { + onCustomItemClick(itemId) + } + } + } + + return ( + + ) +} + +export default Sidebar \ No newline at end of file diff --git a/frontend/data/detectors.json b/frontend/data/detectors.json new file mode 100644 index 0000000..fc05a74 --- /dev/null +++ b/frontend/data/detectors.json @@ -0,0 +1,243 @@ +{ + "objects": { + "object_1": { + "name": "Торговый центр 'Галерея'", + "title": "Торговый центр 'Галерея'", + "description": "Современный торговый центр в центре города", + "image": "/images/test_image.png", + "location": "ул. Лиговский пр., 30А", + "address": "ул. Лиговский пр., 30А", + "floors": 3, + "area": 15000, + "zones": [ + { + "zone_id": "zone_1", + "name": "Зона 1", + "detectors": [4000, 4001, 4004] + }, + { + "zone_id": "zone_2", + "name": "Зона 2", + "detectors": [4002, 4003] + } + ] + }, + "object_2": { + "name": "Офисное здание 'Невский'", + "title": "Офисное здание 'Невский'", + "description": "Многоэтажное офисное здание на Невском проспекте", + "image": "/images/test_image.png", + "location": "Невский пр., 85", + "address": "Невский пр., 85", + "floors": 12, + "area": 8500, + "zones": [ + { + "zone_id": "zone_3", + "name": "Зона 3", + "detectors": [4005, 4006] + } + ] + }, + "object_3": { + "name": "Жилой комплекс 'Северная звезда'", + "title": "Жилой комплекс 'Северная звезда'", + "description": "Высотный жилой комплекс с современными квартирами", + "image": "/images/test_image.png", + "location": "ул. Комендантский пр., 17", + "address": "ул. Комендантский пр., 17", + "floors": 25, + "area": 45000, + "zones": [ + { + "zone_id": "zone_4", + "name": "Зона 4", + "detectors": [4007, 4008] + } + ] + }, + "object_4": { + "name": "Производственный комплекс 'Балтика'", + "title": "Производственный комплекс 'Балтика'", + "description": "Крупный производственный комплекс с современным оборудованием", + "image": "/images/test_image.png", + "location": "Индустриальный пр., 44", + "address": "Индустриальный пр., 44", + "floors": 2, + "area": 12000, + "zones": [ + { + "zone_id": "zone_5", + "name": "Зона 5", + "detectors": [4009] + } + ] + } + }, + "detectors": { + "4000": { + "detector_id": 4000, + "name": "SJ-4000 N81 Мультиплексор", + "object": "object_1", + "status": "#b3261e", + "checked": false, + "type": "fire_detector", + "location": "1 этаж, секция А", + "floor": 1, + "notifications": [ + { + "id": 1, + "type": "critical", + "message": "Критическое превышение температуры", + "timestamp": "2024-01-15T14:30:00Z", + "acknowledged": false, + "priority": "high" + } + ] + }, + "4001": { + "detector_id": 4001, + "name": "SJ-4001 N82 Датчик дыма", + "object": "object_1", + "status": "#fd7c22", + "checked": true, + "type": "fire_detector", + "location": "1 этаж, секция Б", + "floor": 1, + "notifications": [ + { + "id": 2, + "type": "warning", + "message": "Обнаружен дым в помещении", + "timestamp": "2024-01-15T14:25:00Z", + "acknowledged": true, + "priority": "medium" + } + ] + }, + "4002": { + "detector_id": 4002, + "name": "SJ-4002 N83 Тепловой датчик", + "object": "object_1", + "status": "#00ff00", + "checked": true, + "type": "fire_detector", + "location": "2 этаж, секция А", + "floor": 2, + "notifications": [ + { + "id": 3, + "type": "info", + "message": "Плановое техническое обслуживание", + "timestamp": "2024-01-15T14:20:00Z", + "acknowledged": true, + "priority": "low" + } + ] + }, + "4003": { + "detector_id": 4003, + "name": "SJ-4003 N81 Мультиплексор", + "object": "object_1", + "status": "#fd7c22", + "checked": false, + "type": "fire_detector", + "location": "2 этаж, секция Б", + "floor": 2, + "notifications": [] + }, + "4004": { + "detector_id": 4004, + "name": "SJ-4004 N81 Мультиплексор", + "object": "object_1", + "status": "#00ff00", + "checked": true, + "type": "fire_detector", + "location": "3 этаж, секция А", + "floor": 3, + "notifications": [] + }, + "4005": { + "detector_id": 4005, + "name": "SJ-4005 N86 Датчик CO", + "object": "object_2", + "status": "#b3261e", + "checked": false, + "type": "fire_detector", + "location": "3 этаж, секция В", + "floor": 3, + "notifications": [ + { + "id": 4, + "type": "critical", + "message": "Превышение концентрации угарного газа", + "timestamp": "2024-01-15T14:15:00Z", + "acknowledged": false, + "priority": "high" + } + ] + }, + "4006": { + "detector_id": 4006, + "name": "SJ-4006 N81 Мультиплексор", + "object": "object_2", + "status": "#00ff00", + "checked": true, + "type": "fire_detector", + "location": "6 этаж, офис 601", + "floor": 6, + "notifications": [] + }, + "4007": { + "detector_id": 4007, + "name": "SJ-4007 N88 Датчик движения", + "object": "object_3", + "status": "#fd7c22", + "checked": false, + "type": "fire_detector", + "location": "4 этаж, секция А", + "floor": 4, + "notifications": [ + { + "id": 5, + "type": "warning", + "message": "Несанкционированное движение", + "timestamp": "2024-01-15T14:10:00Z", + "acknowledged": false, + "priority": "medium" + } + ] + }, + "4008": { + "detector_id": 4008, + "name": "SJ-4008 N81 Мультиплексор", + "object": "object_3", + "status": "#00ff00", + "checked": false, + "type": "fire_detector", + "location": "15 этаж, квартира 1501", + "floor": 15, + "notifications": [] + }, + "4009": { + "detector_id": 4009, + "name": "SJ-4009 N90 Датчик протечки", + "object": "object_4", + "status": "#00ff00", + "checked": true, + "type": "fire_detector", + "location": "1 этаж, техническое помещение", + "floor": 1, + "notifications": [ + { + "id": 6, + "type": "info", + "message": "Система работает в штатном режиме", + "timestamp": "2024-01-15T14:05:00Z", + "acknowledged": true, + "priority": "low" + } + ] + } + } +} \ No newline at end of file diff --git a/frontend/public/images/test_image.png b/frontend/public/images/test_image.png new file mode 100644 index 0000000000000000000000000000000000000000..9dd3161c4258c244c0109f3ebada914f82a5baa1 GIT binary patch literal 90978 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

%&Om75WOL9{y|NilyBgft#NB2vH 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

    RpJVfCCfmm<2L|=TcC0vkB+s$A1u7xn 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 z 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 + private navigationStore!: NavigationStore + private initialized = false + + init(router: ReturnType, navigationStore: NavigationStore) { + if (this.initialized) { + return // Prevent multiple initializations + } + this.router = router + this.navigationStore = navigationStore + this.initialized = true + } + + isInitialized(): boolean { + return this.initialized + } + + navigateToRoute(route: MainRoutes) { + // Clear any active submenus when navigating to a different route + if (route !== MainRoutes.NAVIGATION) { + this.navigationStore.setCurrentSubmenu(null) + } + + this.router.push(route) + } + + handleSidebarItemClick(itemId: number, currentPath: string): boolean { + if (!this.initialized) { + console.error('NavigationService not initialized!') + return false + } + + const mapping = SIDEBAR_ITEM_MAP[itemId as keyof typeof SIDEBAR_ITEM_MAP] + + if (!mapping) { + return false + } + + if (mapping.type === 'route') { + this.navigateToRoute(mapping.value as MainRoutes) + return true + } + + return false + } + + goBack() { + this.navigationStore.goBack() + } + + selectObjectAndGoToDashboard(objectId: string, objectTitle: string) { + this.navigationStore.setCurrentObject(objectId, objectTitle) + this.navigateToRoute(MainRoutes.DASHBOARD) + } +} + +export const navigationService = new NavigationService() + +export function useNavigationService() { + const router = useRouter() + const navigationStore = useNavigationStore() + + // Initialize only once per component lifecycle + React.useMemo(() => { + if (!navigationService.isInitialized()) { + navigationService.init(router, navigationStore) + } + }, [router, navigationStore]) + + return navigationService +} \ No newline at end of file diff --git a/frontend/utils/meshCache.ts b/frontend/utils/meshCache.ts deleted file mode 100644 index 292094b..0000000 --- a/frontend/utils/meshCache.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { AbstractMesh } from '@babylonjs/core' - -export interface ParsedMeshData { - meshes: unknown[] - boundingBox: { - min: { x: number; y: number; z: number } - max: { x: number; y: number; z: number } - } - metadata: { - modelPath: string - parsedAt: string - totalVertices: number - totalIndices: number - } -} - -export const getCacheFileName = (modelPath: string) => { - const fileName = modelPath.split('/').pop()?.replace('.gltf', '') || 'model' - return `${fileName}_parsed.json` -} - -export const loadCachedData = async (cacheFileName: string): Promise => { - try { - const response = await fetch(`/data/${cacheFileName}`) - if (response.ok) { - const cachedData = await response.json() - console.log('📦 Loaded cached mesh data:', cachedData.metadata) - return cachedData - } - } catch (error) { - console.log('❌ No cached data found, will parse scene') - } - return null -} - -export const extractAllMeshData = (mesh: AbstractMesh) => { - const meshData: Record = {} - - const safeStringify = (obj: unknown): unknown => { - try { - JSON.stringify(obj) - return obj - } catch { - return '[Circular Reference]' - } - } - - const extractValue = (value: unknown): unknown => { - if (value === null || value === undefined) { - return value - } - - if (typeof value === 'function') { - return '[Function]' - } - - if (typeof value === 'object' && value !== null) { - const obj = value as Record - - if (obj.constructor.name === 'Vector3') { - return { x: obj.x, y: obj.y, z: obj.z } - } else if (obj.constructor.name === 'Quaternion') { - return { x: obj.x, y: obj.y, z: obj.z, w: obj.w } - } else if (obj.constructor.name === 'Color3') { - return { r: obj.r, g: obj.g, b: obj.b } - } else if (obj.constructor.name === 'Color4') { - return { r: obj.r, g: obj.g, b: obj.b, a: obj.a } - } else if (Array.isArray(value)) { - return value.map(item => extractValue(item)) - } else { - return safeStringify(value) - } - } - - return value - } - - for (const key in mesh) { - try { - const value = (mesh as unknown as Record)[key] - meshData[key] = extractValue(value) - } catch (error) { - meshData[key] = '[Error accessing property]' - } - } - - meshData.geometry = { - vertices: mesh.getVerticesData('position') ? Array.from(mesh.getVerticesData('position')!) : [], - indices: mesh.getIndices() ? Array.from(mesh.getIndices()!) : [], - normals: mesh.getVerticesData('normal') ? Array.from(mesh.getVerticesData('normal')!) : [], - uvs: mesh.getVerticesData('uv') ? Array.from(mesh.getVerticesData('uv')!) : [], - colors: mesh.getVerticesData('color') ? Array.from(mesh.getVerticesData('color')!) : [], - tangents: mesh.getVerticesData('tangent') ? Array.from(mesh.getVerticesData('tangent')!) : [], - matricesWeights: mesh.getVerticesData('matricesWeights') ? Array.from(mesh.getVerticesData('matricesWeights')!) : [], - matricesIndices: mesh.getVerticesData('matricesIndices') ? Array.from(mesh.getVerticesData('matricesIndices')!) : [], - totalVertices: mesh.getTotalVertices(), - totalIndices: mesh.getIndices() ? mesh.getIndices()!.length : 0 - } - - return meshData -} - -export const parseAndCacheScene = async (meshes: AbstractMesh[], cacheFileName: string, modelPath: string): Promise => { - const parsedMeshes = meshes.map(mesh => extractAllMeshData(mesh)) - - const boundingBox = meshes[0].getHierarchyBoundingVectors() - const totalVertices = parsedMeshes.reduce((sum, mesh) => { - const meshData = mesh as Record - const geometry = meshData.geometry as Record - const vertices = geometry.vertices as number[] - return sum + vertices.length / 3 - }, 0) - const totalIndices = parsedMeshes.reduce((sum, mesh) => { - const meshData = mesh as Record - const geometry = meshData.geometry as Record - const indices = geometry.indices as number[] - return sum + indices.length - }, 0) - - const parsedData: ParsedMeshData = { - meshes: parsedMeshes, - boundingBox: { - min: boundingBox.min, - max: boundingBox.max - }, - metadata: { - modelPath, - parsedAt: new Date().toISOString(), - totalVertices: Math.floor(totalVertices), - totalIndices - } - } - - console.log('💾 Caching parsed mesh data:', parsedData.metadata) - - try { - console.log('💾 Attempting to cache mesh data...') - console.log('💾 Cache file name:', cacheFileName) - console.log('💾 Data size:', JSON.stringify(parsedData).length, 'bytes') - - const response = await fetch('/api/cache-mesh-data', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - fileName: cacheFileName, - data: parsedData - }) - }) - - console.log('💾 Response status:', response.status) - console.log('💾 Response ok:', response.ok) - - if (response.ok) { - const result = await response.json() - console.log('✅ Mesh data cached successfully:', result) - } else { - const errorText = await response.text() - console.warn('⚠️ Failed to cache mesh data:', response.status, errorText) - } - } catch (error) { - console.warn('⚠️ Could not cache mesh data:', error) - console.error('💾 Full error details:', error) - } - - return parsedData -} \ No newline at end of file From ac5d0d0b008ade2f71f8211920fe1dd8cc86ce9a Mon Sep 17 00:00:00 2001 From: iv_vuytsik Date: Thu, 11 Sep 2025 08:12:18 +0300 Subject: [PATCH 2/3] =?UTF-8?q?AEB-55=20/=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BB=D0=BE=D0=B3=D0=BE=20=D0=B8=20?= =?UTF-8?q?=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.gitignore | 3 + frontend/app/(protected)/model/page.tsx | 2 +- frontend/app/(protected)/navigation/page.tsx | 4 +- frontend/app/(protected)/objects/page.tsx | 1 - frontend/app/(protected)/reports/page.tsx | 2 - frontend/components/alerts/DetectorList.tsx | 11 +- frontend/components/dashboard/Dashboard.tsx | 7 +- frontend/components/model/ModelViewer.tsx | 10 +- .../components/navigation/DetectorMenu.tsx | 6 +- .../NotificationDetectorInfo.tsx | 3 +- frontend/components/ui/LoadingSpinner.tsx | 16 +- frontend/components/ui/Sidebar.tsx | 131 +- frontend/public/icons/BellDot.png | Bin 0 -> 346 bytes frontend/public/icons/BookOpen.png | Bin 0 -> 286 bytes frontend/public/icons/Bot.png | Bin 0 -> 328 bytes frontend/public/icons/CircleDot.png | Bin 0 -> 319 bytes frontend/public/icons/History.png | Bin 0 -> 363 bytes frontend/public/icons/Settings2.png | Bin 0 -> 324 bytes frontend/public/icons/SquareTerminal.png | Bin 0 -> 302 bytes frontend/public/icons/logo.png | Bin 0 -> 21008 bytes frontend/public/icons/logo.svg | 9 - ...Viewer_Expo2017Astana_20250908_L_+1430.glb | Bin 0 -> 4129408 bytes .../models/EXPO_АР_PostRecon_level.gltf | 47800 ---------------- frontend/public/models/result.bin | Bin 8021664 -> 0 bytes frontend/services/navigationService.ts | 7 +- 25 files changed, 78 insertions(+), 47934 deletions(-) create mode 100644 frontend/public/icons/BellDot.png create mode 100644 frontend/public/icons/BookOpen.png create mode 100644 frontend/public/icons/Bot.png create mode 100644 frontend/public/icons/CircleDot.png create mode 100644 frontend/public/icons/History.png create mode 100644 frontend/public/icons/Settings2.png create mode 100644 frontend/public/icons/SquareTerminal.png create mode 100644 frontend/public/icons/logo.png delete mode 100644 frontend/public/icons/logo.svg create mode 100644 frontend/public/models/AerBIM-Monitor_ASM-HT-Viewer_Expo2017Astana_20250908_L_+1430.glb delete mode 100644 frontend/public/models/EXPO_АР_PostRecon_level.gltf delete mode 100644 frontend/public/models/result.bin diff --git a/frontend/.gitignore b/frontend/.gitignore index d83c234..c424a30 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -43,3 +43,6 @@ next-env.d.ts # Include data and models folders !/data/ !/public/models/ + +# Exclude models directory (use public/models instead) +/models/ diff --git a/frontend/app/(protected)/model/page.tsx b/frontend/app/(protected)/model/page.tsx index 027d2da..43ee2fe 100644 --- a/frontend/app/(protected)/model/page.tsx +++ b/frontend/app/(protected)/model/page.tsx @@ -24,7 +24,7 @@ export default function Home() { return (
    diff --git a/frontend/app/(protected)/navigation/page.tsx b/frontend/app/(protected)/navigation/page.tsx index f59ff96..2e6ce11 100644 --- a/frontend/app/(protected)/navigation/page.tsx +++ b/frontend/app/(protected)/navigation/page.tsx @@ -219,15 +219,13 @@ const NavigationPage: React.FC = () => { Навигация
    - -
diff --git a/frontend/app/(protected)/objects/page.tsx b/frontend/app/(protected)/objects/page.tsx index ef574ca..4d99ca5 100644 --- a/frontend/app/(protected)/objects/page.tsx +++ b/frontend/app/(protected)/objects/page.tsx @@ -107,7 +107,6 @@ const ObjectsPage: React.FC = () => {
) } - return (
diff --git a/frontend/app/(protected)/reports/page.tsx b/frontend/app/(protected)/reports/page.tsx index 2b81eda..b2e999a 100644 --- a/frontend/app/(protected)/reports/page.tsx +++ b/frontend/app/(protected)/reports/page.tsx @@ -32,8 +32,6 @@ const ReportsPage: React.FC = () => { // TODO: добавить функционал по экспорту отчетов console.log(`Exporting reports as ${format}`) } - - return (
diff --git a/frontend/components/alerts/DetectorList.tsx b/frontend/components/alerts/DetectorList.tsx index 0d0339f..74c3269 100644 --- a/frontend/components/alerts/DetectorList.tsx +++ b/frontend/components/alerts/DetectorList.tsx @@ -12,8 +12,7 @@ interface Detector { floor: number checked: boolean } - -// Interface for raw detector data from JSON + interface RawDetector { detector_id: number name: string @@ -53,9 +52,7 @@ const DetectorList: React.FC = ({ objectId, selectedDetectors }, [objectId]) - - - + const filteredDetectors = detectors.filter(detector => { const matchesSearch = detector.name.toLowerCase().includes(searchTerm.toLowerCase()) || detector.location.toLowerCase().includes(searchTerm.toLowerCase()) @@ -67,9 +64,7 @@ const DetectorList: React.FC = ({ objectId, selectedDetectors return matchesSearch }) - - - + return (
diff --git a/frontend/components/dashboard/Dashboard.tsx b/frontend/components/dashboard/Dashboard.tsx index 475a54d..7f26cf5 100644 --- a/frontend/components/dashboard/Dashboard.tsx +++ b/frontend/components/dashboard/Dashboard.tsx @@ -51,17 +51,14 @@ const Dashboard: React.FC = () => { return acc }, { critical: 0, warning: 0, normal: 0 }) - const handleNavigationClick = () => { - // Close all submenus before navigating + const handleNavigationClick = () => { closeMonitoring() closeFloorNavigation() closeNotifications() setCurrentSubmenu(null) router.push('/navigation') } - - // No custom sidebar handling needed - using unified navigation service - + return (
= ({ const [showModel, setShowModel] = useState(false) const isInitializedRef = useRef(false) const isDisposedRef = useRef(false) - - + useEffect(() => { isDisposedRef.current = false @@ -154,7 +153,11 @@ const ModelViewer: React.FC = ({ setLoadingProgress(100) console.log('GLTF Model loaded successfully!') - + console.log('\n=== Complete Model Object ===') + console.log(result) + console.log('\n=== Structure Overview ===') + console.log('Meshes:', result.meshes?.length || 0) + console.log('Transform Nodes:', result.transformNodes?.length || 0) if (result.meshes.length > 0) { const boundingBox = result.meshes[0].getHierarchyBoundingVectors() @@ -165,7 +168,6 @@ const ModelViewer: React.FC = ({ camera.radius = maxDimension * 2 camera.target = result.meshes[0].position - onModelLoaded?.({ meshes: result.meshes, boundingBox: { diff --git a/frontend/components/navigation/DetectorMenu.tsx b/frontend/components/navigation/DetectorMenu.tsx index 6f892ac..9bcffa2 100644 --- a/frontend/components/navigation/DetectorMenu.tsx +++ b/frontend/components/navigation/DetectorMenu.tsx @@ -26,7 +26,6 @@ const DetectorMenu: React.FC = ({ detector, isOpen, onClose, return (
- {/* Header */}

Датч.{detector.name} @@ -47,7 +46,7 @@ const DetectorMenu: React.FC = ({ detector, isOpen, onClose,

- {/* Detector Information Table */} + {/* Таблица детекторов */}
@@ -79,8 +78,7 @@ const DetectorMenu: React.FC = ({ detector, isOpen, onClose,
- - {/* Close Button */} +