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 (
+
+ {value === index && (
+
+ {children}
+
+ )}
+
+ );
+};
+
+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 для новых вкладок */}
+
+
+
+
+ : }
+ >
+ {isSaving ? 'Сохранение...' : 'Сохранить'}
+
+
+
+
+ {/* Уведомление об успешном сохранении */}
+ setShowSuccess(false)}
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
+ >
+ setShowSuccess(false)} severity="success" sx={{ width: '100%' }}>
+ Настройки успешно сохранены!
+
+
+
+ {/* Диалог подтверждения закрытия */}
+
+ >
+ );
+};
+
+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}
/>
);
};