From 69a5e4ade1c576584c2f062f25149c7447c223ab Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 10 Jun 2025 09:29:14 -0400 Subject: [PATCH 1/3] sidebar menu improvement --- src/Components/Layout/SidebarMenuWrapper.jsx | 76 +++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/Components/Layout/SidebarMenuWrapper.jsx b/src/Components/Layout/SidebarMenuWrapper.jsx index 22255f9..2980c64 100644 --- a/src/Components/Layout/SidebarMenuWrapper.jsx +++ b/src/Components/Layout/SidebarMenuWrapper.jsx @@ -5,16 +5,54 @@ 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); + // Загружаем меню из 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'); @@ -26,6 +64,40 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => { fetchMenuData(); }, []); + // Фоновая проверка обновлений + 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 { const response = await axios.put( From 328018edfaa9fd608dcf4fe40967d027bec82ea5 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 11 Jun 2025 04:49:48 -0400 Subject: [PATCH 2/3] created modal window for settings --- .../Layout/SettingsComponents/RangeEditor.jsx | 153 ++++++++++++++ src/Components/Layout/SettingsModal.jsx | 196 ++++++++++++++++++ src/Components/Layout/SidebarMenu.jsx | 78 +------ .../Layout/SidebarMenuComponents/MenuItem.jsx | 25 +-- .../SidebarMenuComponents/SidebarFooter.jsx | 116 +++++++---- 5 files changed, 425 insertions(+), 143 deletions(-) create mode 100644 src/Components/Layout/SettingsComponents/RangeEditor.jsx create mode 100644 src/Components/Layout/SettingsModal.jsx diff --git a/src/Components/Layout/SettingsComponents/RangeEditor.jsx b/src/Components/Layout/SettingsComponents/RangeEditor.jsx new file mode 100644 index 0000000..d0c3d65 --- /dev/null +++ b/src/Components/Layout/SettingsComponents/RangeEditor.jsx @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Button, + TextField, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Typography, + Divider, + Select, + MenuItem, + FormControl, + InputLabel +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; + +const RangeEditor = ({ metric, onSave, onCancel }) => { + const [ranges, setRanges] = useState([...metric.ranges]); + const [newRange, setNewRange] = useState({ min: 0, max: 0, status: 1 }); + + const handleAddRange = () => { + if (newRange.min >= newRange.max) { + alert('Минимальное значение должно быть меньше максимального'); + return; + } + + // Проверка на пересечение с существующими диапазонами + const overlaps = ranges.some(range => + (newRange.min >= range.min && newRange.min <= range.max) || + (newRange.max >= range.min && newRange.max <= range.max) + ); + + if (overlaps) { + alert('Диапазон пересекается с существующим'); + return; + } + + setRanges([...ranges, newRange].sort((a, b) => a.min - b.min)); + setNewRange({ min: 0, max: 0, status: 1 }); + }; + + const handleDeleteRange = (index) => { + const newRanges = [...ranges]; + newRanges.splice(index, 1); + setRanges(newRanges); + }; + + const handleSave = () => { + onSave({ + ...metric, + ranges + }); + }; + + return ( + + + Редактирование диапазонов для: {metric.name} + + + + + + + Минимум + Максимум + Статус + Действия + + + + {ranges.map((range, index) => ( + + {range.min} + {range.max} + {range.status} + + handleDeleteRange(index)}> + + + + + ))} + +
+
+ + + + + Добавить новый диапазон + + + + setNewRange({ ...newRange, min: parseInt(e.target.value) || 0 })} + size="small" + /> + + setNewRange({ ...newRange, max: parseInt(e.target.value) || 0 })} + size="small" + /> + + + Статус + + + + + + + + + + +
+ ); +}; + +export default RangeEditor; \ 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..f9f7b6f --- /dev/null +++ b/src/Components/Layout/SettingsModal.jsx @@ -0,0 +1,196 @@ +// 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'; + +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 }) => { + 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 handleTabChange = (event, newValue) => { + if (hasChanges) { + setShowConfirmClose(true); + } else { + setTabValue(newValue); + } + }; + + const handleSave = () => { + setIsSaving(true); + // Имитация асинхронного сохранения + setTimeout(() => { + setIsSaving(false); + setShowSuccess(true); + setHasChanges(false); + }, 1500); + }; + + 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..2617bda 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,7 @@ const SidebarMenu = ({ data, isDarkMode, setIsDarkMode, - onEditItem, - onSelectItem, - editModalOpen, - editingItem, - onCloseEditModal, - onSaveChanges + onSelectItem }) => { const [collapsed, setCollapsed] = useState(false); const { sidebarWidth, startResizing } = useSidebarResize(290); @@ -148,7 +136,7 @@ const SidebarMenu = ({ item={data} collapsed={collapsed} level={0} - onEdit={onEditItem} + onSelectItem={onSelectItem} /> )} @@ -160,74 +148,12 @@ const SidebarMenu = ({ setIsDarkMode={setIsDarkMode} /> - {!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..66a37e7 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, @@ -28,51 +31,76 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({ })); const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => { - return ( - - {!collapsed && ( - - - - )} - - {!collapsed && ( - - )} + 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 From 405bda3df9eaf9905cea6cf0595f091cdac3edeb Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 11 Jun 2025 07:30:34 -0400 Subject: [PATCH 3/3] added ranages editor --- .../SettingsComponents/MetricRangeEditor.jsx | 258 ++++++++++++++++++ .../Layout/SettingsComponents/RangeEditor.jsx | 153 ----------- src/Components/Layout/SettingsModal.jsx | 55 ++-- src/Components/Layout/SidebarMenu.jsx | 4 +- .../SidebarMenuComponents/SidebarFooter.jsx | 6 +- src/Components/Layout/SidebarMenuWrapper.jsx | 10 +- 6 files changed, 311 insertions(+), 175 deletions(-) create mode 100644 src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx delete mode 100644 src/Components/Layout/SettingsComponents/RangeEditor.jsx 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/SettingsComponents/RangeEditor.jsx b/src/Components/Layout/SettingsComponents/RangeEditor.jsx deleted file mode 100644 index d0c3d65..0000000 --- a/src/Components/Layout/SettingsComponents/RangeEditor.jsx +++ /dev/null @@ -1,153 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - Box, - Button, - TextField, - IconButton, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - Typography, - Divider, - Select, - MenuItem, - FormControl, - InputLabel -} from '@mui/material'; -import AddIcon from '@mui/icons-material/Add'; -import DeleteIcon from '@mui/icons-material/Delete'; - -const RangeEditor = ({ metric, onSave, onCancel }) => { - const [ranges, setRanges] = useState([...metric.ranges]); - const [newRange, setNewRange] = useState({ min: 0, max: 0, status: 1 }); - - const handleAddRange = () => { - if (newRange.min >= newRange.max) { - alert('Минимальное значение должно быть меньше максимального'); - return; - } - - // Проверка на пересечение с существующими диапазонами - const overlaps = ranges.some(range => - (newRange.min >= range.min && newRange.min <= range.max) || - (newRange.max >= range.min && newRange.max <= range.max) - ); - - if (overlaps) { - alert('Диапазон пересекается с существующим'); - return; - } - - setRanges([...ranges, newRange].sort((a, b) => a.min - b.min)); - setNewRange({ min: 0, max: 0, status: 1 }); - }; - - const handleDeleteRange = (index) => { - const newRanges = [...ranges]; - newRanges.splice(index, 1); - setRanges(newRanges); - }; - - const handleSave = () => { - onSave({ - ...metric, - ranges - }); - }; - - return ( - - - Редактирование диапазонов для: {metric.name} - - - - - - - Минимум - Максимум - Статус - Действия - - - - {ranges.map((range, index) => ( - - {range.min} - {range.max} - {range.status} - - handleDeleteRange(index)}> - - - - - ))} - -
-
- - - - - Добавить новый диапазон - - - - setNewRange({ ...newRange, min: parseInt(e.target.value) || 0 })} - size="small" - /> - - setNewRange({ ...newRange, max: parseInt(e.target.value) || 0 })} - size="small" - /> - - - Статус - - - - - - - - - - -
- ); -}; - -export default RangeEditor; \ No newline at end of file diff --git a/src/Components/Layout/SettingsModal.jsx b/src/Components/Layout/SettingsModal.jsx index f9f7b6f..c61340c 100644 --- a/src/Components/Layout/SettingsModal.jsx +++ b/src/Components/Layout/SettingsModal.jsx @@ -19,6 +19,7 @@ import { } 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 ; @@ -52,12 +53,16 @@ const TabPanel = (props) => { ); }; -const SettingsModal = ({ open, onClose }) => { +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) { @@ -67,14 +72,29 @@ const SettingsModal = ({ open, onClose }) => { } }; - const handleSave = () => { + const handleSave = async () => { setIsSaving(true); - // Имитация асинхронного сохранения - setTimeout(() => { + 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); - setShowSuccess(true); - setHasChanges(false); - }, 1500); + } + }; + + const handleMetricEditorChange = ({ hasChanges, saveChanges }) => { + setMetricEditorState({ hasChanges, save: saveChanges }); + setHasChanges(hasChanges); }; const handleClose = () => { @@ -122,34 +142,33 @@ const SettingsModal = ({ open, onClose }) => { - + - + {/* Добавляйте новые вкладки здесь */} - + Настройки меню {/* Добавьте содержимое для вкладки меню */} - + - Настройки внешнего вида - {/* Добавьте содержимое для вкладки внешнего вида */} + - + {/* Добавляйте новые TabPanel для новых вкладок */} - + -