diff --git a/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx b/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx new file mode 100644 index 0000000..62b4471 --- /dev/null +++ b/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx @@ -0,0 +1,258 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + TextField, Box, Typography, IconButton, Divider, + CircularProgress, Alert, Collapse, Tooltip, Button +} from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from '@mui/icons-material/Add'; +import SearchIcon from '@mui/icons-material/Search'; +import axios from 'axios'; + +const MetricRangeEditor = ({ onSave }) => { + const [ranges, setRanges] = useState([]); + const [filter, setFilter] = useState(''); + const [newMetricName, setNewMetricName] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [hasChanges, setHasChanges] = useState(false); + const [success, setSuccess] = useState(false); + + // Загрузка данных + const loadRanges = useCallback(async () => { + try { + setLoading(true); + const res = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/ranges/list`); + setRanges( + Object.entries(res.data).map(([name, r]) => ({ + name, + ranges: Array.isArray(r) ? r : [] + })) + ); + setError(null); + } catch (err) { + console.error('Ошибка при получении ranges:', err); + setError('Не удалось загрузить данные'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadRanges(); + }, [loadRanges]); + + // Обновление диапазона + const updateRange = (metricIndex, rangeIndex, field, value) => { + const newRanges = [...ranges]; + newRanges[metricIndex].ranges[rangeIndex][field] = Number(value); + setRanges(newRanges); + setHasChanges(true); + }; + + // Добавление диапазона + const addRange = (metricIndex) => { + const newRanges = [...ranges]; + newRanges[metricIndex].ranges.push({ min: 0, max: 100, status: 1 }); + setRanges(newRanges); + setHasChanges(true); + }; + + // Удаление диапазона + const deleteRange = (metricIndex, rangeIndex) => { + const newRanges = [...ranges]; + newRanges[metricIndex].ranges.splice(rangeIndex, 1); + setRanges(newRanges); + setHasChanges(true); + }; + + const saveChanges = async () => { + try { + setLoading(true); + await axios.post(`${import.meta.env.VITE_BACK_URL}/api/ranges/update`, ranges); + setHasChanges(false); + setSuccess(true); + setTimeout(() => setSuccess(false), 3000); + + if (onSave) { + onSave({ + hasChanges: false, + saveChanges: saveChanges + }); + } + return true; + } catch (err) { + console.error('Ошибка при сохранении:', err); + setError('Ошибка при сохранении'); + return false; + } finally { + setLoading(false); + } + }; + + // Добавление новой метрики + const addNewMetric = () => { + if (!newMetricName.trim()) { + setError('Введите название метрики'); + return; + } + if (ranges.some(r => r.name === newMetricName)) { + setError('Метрика с таким именем уже существует'); + return; + } + setRanges([...ranges, { + name: newMetricName, + ranges: [{ min: 0, max: 100, status: 1 }] + }]); + setNewMetricName(''); + setHasChanges(true); + setError(null); + }; + + const filtered = filter + ? ranges.filter(r => r.name.toLowerCase().includes(filter.toLowerCase())) + : ranges; + + return ( + + {loading && ( + + + + )} + + + setError(null)}> + {error} + + + + + setSuccess(false)}> + Изменения успешно сохранены! + + + + {!loading && ( + <> + + + setFilter(e.target.value)} + variant="standard" + /> + + + + setNewMetricName(e.target.value)} + fullWidth + variant="standard" + /> + + + + + + + + + + {filtered.map((metric, i) => ( + + + {metric.name} + + + {metric.ranges.map((r, j) => ( + *': { flex: 1 } + }} + > + updateRange(i, j, 'min', e.target.value)} + size="small" + /> + updateRange(i, j, 'max', e.target.value)} + size="small" + /> + updateRange(i, j, 'status', e.target.value)} + size="small" + /> + + deleteRange(i, j)} + size="small" + sx={{ flex: 'none' }} + > + + + + + ))} + + + + ))} + + {filtered.length === 0 && ( + + {filter ? 'Ничего не найдено' : 'Нет метрик для отображения'} + + )} + + )} + + {/* Передаем состояние изменений в родительский компонент */} + {useEffect(() => { + if (onSave) { + onSave({ hasChanges, saveChanges }); + } + }, [hasChanges, onSave])} + + ); +}; + +export default MetricRangeEditor; \ No newline at end of file diff --git a/src/Components/Layout/SettingsModal.jsx b/src/Components/Layout/SettingsModal.jsx new file mode 100644 index 0000000..c61340c --- /dev/null +++ b/src/Components/Layout/SettingsModal.jsx @@ -0,0 +1,215 @@ +// components/SettingsModal.jsx +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Tabs, + Tab, + Box, + Typography, + IconButton, + styled, + CircularProgress, + Slide, + Snackbar, + Alert +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import SaveIcon from '@mui/icons-material/Save'; +import MetricRangeEditor from './SettingsComponents/MetricRangeEditor'; + +const Transition = React.forwardRef(function Transition(props, ref) { + return ; +}); + +const StyledDialog = styled(Dialog)(({ theme }) => ({ + '& .MuiDialog-paper': { + minWidth: 600, + maxHeight: '80vh', + backgroundColor: theme.palette.background.paper, + }, +})); + +const TabPanel = (props) => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + +const SettingsModal = ({ open, onClose, onMenuUpdate }) => { + const [tabValue, setTabValue] = useState(0); + const [isSaving, setIsSaving] = useState(false); + const [showSuccess, setShowSuccess] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [showConfirmClose, setShowConfirmClose] = useState(false); + const [metricEditorState, setMetricEditorState] = useState({ + hasChanges: false, + save: () => { } + }); + + const handleTabChange = (event, newValue) => { + if (hasChanges) { + setShowConfirmClose(true); + } else { + setTabValue(newValue); + } + }; + + const handleSave = async () => { + setIsSaving(true); + try { + let success = true; + if (tabValue === 1 && metricEditorState.hasChanges) { + success = await metricEditorState.save(); + } + + if (success) { + setShowSuccess(true); + setHasChanges(false); + if (onMenuUpdate) { + onMenuUpdate(); + } + } + } finally { + setIsSaving(false); + } + }; + + const handleMetricEditorChange = ({ hasChanges, saveChanges }) => { + setMetricEditorState({ hasChanges, save: saveChanges }); + setHasChanges(hasChanges); + }; + + const handleClose = () => { + if (hasChanges) { + setShowConfirmClose(true); + } else { + onClose(); + } + }; + + const handleConfirmClose = (shouldClose) => { + setShowConfirmClose(false); + if (shouldClose) { + onClose(); + } + }; + + // Пример обработчика изменений + const handleSettingChange = () => { + setHasChanges(true); + }; + + return ( + <> + + + Настройки + theme.palette.grey[500], + }} + > + + + + + + + + + {/* Добавляйте новые вкладки здесь */} + + + + + + Настройки меню + {/* Добавьте содержимое для вкладки меню */} + + + + + + + {/* Добавляйте новые TabPanel для новых вкладок */} + + + + + + + + + {/* Уведомление об успешном сохранении */} + setShowSuccess(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + > + setShowSuccess(false)} severity="success" sx={{ width: '100%' }}> + Настройки успешно сохранены! + + + + {/* Диалог подтверждения закрытия */} + handleConfirmClose(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + Есть несохраненные изменения + + Вы уверены, что хотите закрыть без сохранения изменений? + + + + + + + + ); +}; + +export default SettingsModal; \ No newline at end of file diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx index e37f326..d592dc9 100644 --- a/src/Components/Layout/SidebarMenu.jsx +++ b/src/Components/Layout/SidebarMenu.jsx @@ -3,17 +3,10 @@ import React, { useState, useEffect } from "react"; import { Drawer, List, - Typography, styled, IconButton, Tooltip, Box, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, - TextField } from "@mui/material"; import MenuItem from "./SidebarMenuComponents/MenuItem"; import SidebarFooter from "./SidebarMenuComponents/SidebarFooter"; @@ -27,12 +20,8 @@ const SidebarMenu = ({ data, isDarkMode, setIsDarkMode, - onEditItem, onSelectItem, - editModalOpen, - editingItem, - onCloseEditModal, - onSaveChanges + forceRefreshMenu }) => { const [collapsed, setCollapsed] = useState(false); const { sidebarWidth, startResizing } = useSidebarResize(290); @@ -148,7 +137,7 @@ const SidebarMenu = ({ item={data} collapsed={collapsed} level={0} - onEdit={onEditItem} + onSelectItem={onSelectItem} /> )} @@ -158,76 +147,15 @@ const SidebarMenu = ({ collapsed={collapsed} isDarkMode={isDarkMode} setIsDarkMode={setIsDarkMode} + forceRefreshMenu={forceRefreshMenu} /> - {!collapsed && ( )} - - {/* Модальное окно редактирования */} - ); }; -const EditMenuItemDialog = ({ open, item, onClose, onSave }) => { - const [formData, setFormData] = useState(item || {}); - - useEffect(() => { - setFormData(item || {}); - }, [item]); - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - }; - - const handleSubmit = () => { - onSave(formData); - }; - - if (!item) return null; - - return ( - - Редактирование элемента меню - - - - - {/* Дополнительные поля для редактирования */} - - - - - - - - ); -}; - export default SidebarMenu; \ No newline at end of file diff --git a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx index 6fd6607..afb8801 100644 --- a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx +++ b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx @@ -7,11 +7,10 @@ import { Collapse, List, styled, - IconButton, Menu, MenuItem as MuiMenuItem } from "@mui/material"; -import { ExpandLess, ExpandMore, Folder, FolderOpen, Edit } from "@mui/icons-material"; +import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material"; import StatusIndicator from "./StatusIndicator"; const StyledListItem = styled(ListItem)(({ theme, level }) => ({ @@ -44,11 +43,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => { setContextMenu(null); }; - const handleEditClick = () => { - onEdit(item); - handleCloseContextMenu(); - }; - const handleToggle = (e) => { e.stopPropagation(); setIsOpen(!isOpen); @@ -87,19 +81,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => { }} /> {hasChildren && (isOpen ? : )} - - {level > 0 && ( - { - e.stopPropagation(); - handleEditClick(); - }} - sx={{ ml: 1 }} - > - - - )} )} @@ -114,9 +95,7 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => { : undefined } > - - Редактировать - + {hasChildren && !collapsed && ( diff --git a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx index 54a2b7b..d9546ff 100644 --- a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx +++ b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx @@ -1,4 +1,5 @@ -import React from "react"; +// components/SidebarMenuComponents/SidebarFooter.jsx +import React, { useState } from "react"; import { Brightness4, Brightness7 } from "@mui/icons-material"; import { IconButton, Tooltip } from "@mui/material"; import { @@ -7,8 +8,10 @@ import { ListItemText, styled, Switch, - Box + Box, + Button } from "@mui/material"; +import SettingsModal from "../SettingsModal"; const FooterList = styled(List)(({ theme }) => ({ backgroundColor: theme.palette.custom.sidebar, @@ -27,52 +30,79 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({ alignItems: 'center' })); -const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => { - return ( - - {!collapsed && ( - - - - )} - - {!collapsed && ( - - )} +const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu }) => { + const [settingsOpen, setSettingsOpen] = useState(false); - - - setIsDarkMode(!isDarkMode)} - sx={{ color: 'custom.sidebarText' }} - > - {isDarkMode ? : } - - - {!collapsed && ( - setIsDarkMode(!isDarkMode)} - size="small" + const handleSettingsOpen = () => { + setSettingsOpen(true); + }; + + const handleSettingsClose = () => { + setSettingsOpen(false); + }; + + return ( + <> + + {!collapsed && ( + + + + )} + + {!collapsed && ( + )} - - - + + + + setIsDarkMode(!isDarkMode)} + sx={{ color: 'custom.sidebarText' }} + > + {isDarkMode ? : } + + + {!collapsed && ( + setIsDarkMode(!isDarkMode)} + size="small" + /> + )} + + + + + + ); }; -export default SidebarFooter; +export default SidebarFooter; \ No newline at end of file diff --git a/src/Components/Layout/SidebarMenuWrapper.jsx b/src/Components/Layout/SidebarMenuWrapper.jsx index 22255f9..ddce8fa 100644 --- a/src/Components/Layout/SidebarMenuWrapper.jsx +++ b/src/Components/Layout/SidebarMenuWrapper.jsx @@ -5,16 +5,61 @@ import axios from 'axios'; const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => { const [menuData, setMenuData] = useState(null); + const [lastModified, setLastModified] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editingItem, setEditingItem] = useState(null); const [editModalOpen, setEditModalOpen] = useState(false); + const [backgroundLoading, setBackgroundLoading] = useState(false); + const [refreshTrigger, setRefreshTrigger] = useState(0); + + const forceRefreshMenu = () => { + setRefreshTrigger(prev => prev + 1); + localStorage.removeItem('menuCache'); // Очищаем кэш + }; + + // Загружаем меню из localStorage при инициализации + useEffect(() => { + const loadCachedMenu = () => { + try { + const cached = localStorage.getItem('menuCache'); + if (cached) { + const { data, timestamp } = JSON.parse(cached); + setMenuData(data); + setLastModified(timestamp); + } + } catch (e) { + console.warn('Failed to load menu from cache', e); + } + }; + + loadCachedMenu(); + }, []); + + // Основная загрузка меню useEffect(() => { const fetchMenuData = async () => { try { - const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`); - setMenuData(response.data); // axios хранит данные в response.data + setLoading(true); + const headers = lastModified ? { 'If-Modified-Since': lastModified } : {}; + + const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`, { + headers, + validateStatus: status => status === 200 || status === 304 + }); + + if (response.status === 200) { + const newLastModified = response.headers['last-modified']; + setMenuData(response.data); + setLastModified(newLastModified); + + // Сохраняем в кэш + localStorage.setItem('menuCache', JSON.stringify({ + data: response.data, + timestamp: newLastModified + })); + } } catch (err) { console.error('Error fetching menu data:', err); setError(err.message || 'Failed to fetch menu data'); @@ -24,7 +69,41 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => { }; fetchMenuData(); - }, []); + }, [refreshTrigger]); + + // Фоновая проверка обновлений + useEffect(() => { + if (!lastModified) return; + + const checkForUpdates = async () => { + try { + setBackgroundLoading(true); + const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/check-updates`, { + headers: { 'If-Modified-Since': lastModified } + }); + + if (response.data.hasUpdates) { + // Если есть обновления, загружаем их в фоне + const updateResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`); + setMenuData(updateResponse.data); + setLastModified(updateResponse.headers['last-modified']); + + localStorage.setItem('menuCache', JSON.stringify({ + data: updateResponse.data, + timestamp: updateResponse.headers['last-modified'] + })); + } + } catch (err) { + console.warn('Background update check failed', err); + } finally { + setBackgroundLoading(false); + } + }; + + // Проверяем обновления каждые 5 минут + const interval = setInterval(checkForUpdates, 5 * 60 * 1000); + return () => clearInterval(interval); + }, [lastModified]); const handleSaveChanges = async (updatedItem) => { try { @@ -97,6 +176,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => { editingItem={editingItem} onCloseEditModal={() => setEditModalOpen(false)} onSaveChanges={handleSaveChanges} + forceRefreshMenu={forceRefreshMenu} /> ); };