diff --git a/web/src/api/users.js b/web/src/api/users.js index b75a70f..73c2824 100644 --- a/web/src/api/users.js +++ b/web/src/api/users.js @@ -1,14 +1,16 @@ -import api from './client'; +import api from "./client"; export const UsersApi = { me() { - return api.get('/users/me').then((r) => r.data); + return api.get("/users/me").then((r) => r.data); }, getById(userId) { return api.get(`/users/${userId}`).then((r) => r.data); }, getAll() { - return api.get('/users/').then((r) => r.data); + return api + .get("/users/", { params: { limit: 1000, skip: 0 } }) + .then((r) => r.data); }, update(idUser, data) { return api.patch(`/users/${idUser}`, data).then((r) => r.data); diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index 4eb663a..041acdc 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -1,79 +1,210 @@ -import React from 'react'; -import { useMemo } from 'react'; -import styled from '@emotion/styled'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Box, Tooltip, CircularProgress } from '@mui/material'; +import { useRealtime } from '../../contexts/RealtimeContext'; -const ContainerForText = styled.span` - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; - overflow: hidden; - white-space: pre-wrap; -`; +const Cell = ({ value, row, column, onSave, userId, isEditable = true }) => { + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(value); + const [isSaving, setIsSaving] = useState(false); + const [lockInfo, setLockInfo] = useState(null); + + const { + isCellLocked, + getCellLockInfo, + updateCell, + subscribeToErrors, + tryLockCell, + unlockCell + } = useRealtime(); + + const inputRef = useRef(null); + const rowId = row.original?.id || row.id || row.index; + const columnId = column.id; -// Функция для подсветки текста -const highlightText = (text, searchQuery) => { - if (!searchQuery || !text) return text; + // Проверяем блокировку ячейки + useEffect(() => { + if (!isEditing) { + const checkLock = () => { + const lock = getCellLockInfo(rowId, columnId); + setLockInfo(lock); + }; + + checkLock(); + const interval = setInterval(checkLock, 2000); // Проверяем каждые 2 секунды + return () => clearInterval(interval); + } + }, [rowId, columnId, isEditing, getCellLockInfo]); - // Экранируем спецсимволы в поисковом запросе - const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const regex = new RegExp(`(${escapedQuery})`, 'gi'); + // Подписка на ошибки + useEffect(() => { + const unsubscribe = subscribeToErrors((error) => { + console.error('Cell update error:', error); + setIsSaving(false); + setIsEditing(false); + }); + + return unsubscribe; + }, [subscribeToErrors]); - const parts = text.split(regex); + const isLocked = isCellLocked(rowId, columnId); + const isLockedByMe = lockInfo?.userId === userId; - return parts.map((part, index) => - regex.test(part) ? ( - { + if (!isEditable) return; + + if (!isLocked || isLockedByMe) { + // Пытаемся заблокировать ячейку + if (tryLockCell(rowId, columnId, userId)) { + setIsEditing(true); + setEditValue(value); + setTimeout(() => inputRef.current?.focus(), 100); + } else { + const lock = getCellLockInfo(rowId, columnId); + console.warn(`Cell is locked by user ${lock?.userId}`); + } + } + }, [isEditable, isLocked, isLockedByMe, tryLockCell, rowId, columnId, userId, value, getCellLockInfo]); + + const handleSave = useCallback(async () => { + if (editValue === value) { + setIsEditing(false); + unlockCell(rowId, columnId, userId); + return; + } + + setIsSaving(true); + const success = await updateCell(rowId, columnId, editValue); + + if (success) { + onSave?.(row, column, editValue); + setIsEditing(false); + } else { + setIsSaving(false); + } + }, [editValue, value, updateCell, rowId, columnId, userId, onSave, row, column, unlockCell]); + + const handleKeyDown = useCallback((e) => { + if (e.key === 'Enter') { + handleSave(); + } else if (e.key === 'Escape') { + setIsEditing(false); + setEditValue(value); + unlockCell(rowId, columnId, userId); + } + }, [handleSave, value, rowId, columnId, userId, unlockCell]); + + // Отмена блокировки при размонтировании, если ячейка редактировалась + useEffect(() => { + return () => { + if (isEditing) { + unlockCell(rowId, columnId, userId); + } + }; + }, [isEditing, rowId, columnId, userId, unlockCell]); + + const getLockTooltip = () => { + if (!isEditable) return 'This cell is not editable'; + if (isLocked && !isLockedByMe) { + return `Editing by user ${lockInfo?.userId}`; + } + if (isLockedByMe && isEditing) { + return 'You are editing this cell'; + } + return 'Double-click to edit'; + }; + + const getCellStyle = () => { + const baseStyle = { + padding: '8px', + minHeight: '40px', + position: 'relative', + }; + + if (!isEditable) { + return { + ...baseStyle, + backgroundColor: '#fafafa', + color: '#999', + cursor: 'not-allowed', + }; + } + + if (isLocked && !isLockedByMe) { + return { + ...baseStyle, + backgroundColor: '#f5f5f5', + opacity: 0.7, + cursor: 'not-allowed', + }; + } + + return { + ...baseStyle, + cursor: 'text', + '&:hover': { + backgroundColor: '#f0f0f0', + }, + }; + }; + + if (isEditing) { + return ( + + setEditValue(e.target.value)} + onBlur={handleSave} + onKeyDown={handleKeyDown} + disabled={isSaving} + style={{ + width: '100%', + padding: '8px', + border: '2px solid #1976d2', + borderRadius: '4px', + fontSize: 'inherit', + outline: 'none', + }} + /> + {isSaving && ( + + )} + + ); + } + + return ( + + - {part} - - ) : ( - part - ), + {value || ''} + {isLockedByMe && !isEditing && ( + + )} + + ); }; -const Cell = React.memo(({ cell, onClick, globalFilter, backgroundColor, color }) => { - const cellValue = cell.getValue(); - const textValue = String(cellValue || ''); - - // Подсвечиваем текст с учетом поискового запроса - const highlightedContent = useMemo(() => { - return highlightText(textValue, globalFilter); - }, [textValue, globalFilter]); - - return ( -
- {highlightedContent} -
- ); -}); - -export default Cell; +export default Cell; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 81ff7e1..6287157 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -4,8 +4,9 @@ import { useMaterialReactTable, MaterialReactTable, } from 'material-react-table'; +import { CircularProgress, Typography, Box } from '@mui/material'; -import useRealtimeData from './useRealtimeData'; +import useRealtimeData from './hooks/useRealtimeData'; import { getTableColumns } from './tableColumns'; import { Cell, EditCell } from './index'; import { TableHead } from './TableHead/TableHead'; @@ -20,12 +21,29 @@ import { getTablePaperStyles, } from './constants/tableConfig'; -const RealtimeTable = ({ formType, formId, sheetName }) => { - const { data, setData } = useRealtimeData(formId, sheetName); - const [isLoading, setIsLoading] = useState(true); +// Импортируем WebSocket контекст +import { RealtimeProvider, useRealtime } from './contexts/RealtimeContext'; + +// Внутренний компонент, который будет использовать WebSocket +const RealtimeTableContent = ({ formType, formId, sheetName }) => { + // Получаем данные и состояние из хука + const { + data, + setData, + isConnected, + isLoading: isDataLoading, + error: wsError + } = useRealtimeData(formId, sheetName); + // isConnected = true + + // Получаем функции из WebSocket контекста + const { login, releaseAllLocks } = useRealtime(); + const [globalFilter, setGlobalFilter] = useState(''); const [showColumnFilters, setShowColumnFilters] = useState(false); const [selectedColumn, setSelectedColumn] = useState(); + const [columnsConfig, setColumnsConfig] = useState(null); + const [userId, setUserId] = useState(null); const columnVirtualizerInstanceRef = useRef(null); @@ -42,18 +60,35 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { } = useColumnSettings(); const { headerPortalRef, containerRef } = useHeaderPortal(); + const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale(containerRef); - const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = - useTableScale(containerRef); + // Получаем ID пользователя + useEffect(() => { + const getUserId = async () => { + try { + const token = localStorage.getItem('access_token'); + if (token) { + // Декодируем JWT токен + const payload = JSON.parse(atob(token.split('.')[1])); + setUserId(payload.user_id || payload.sub || payload.id); + } else { + // Для разработки + setUserId('anonymous_' + Date.now()); + } + } catch (error) { + console.error('Failed to get user ID:', error); + setUserId('anonymous_' + Date.now()); + } + }; - const [columnsConfig, setColumnsConfig] = useState(null); + getUserId(); + }, []); + // Загрузка конфигурации колонок useEffect(() => { const loadConfig = async () => { try { const { config } = await import(`./constants/${formType}/${sheetName}.js`); - - // Make sure config.configData has the expected structure setColumnsConfig(config.config); } catch (error) { console.error(`Failed to load data for ${formType}/${sheetName}:`, error); @@ -64,30 +99,66 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { loadConfig(); }, [formType, sheetName]); + // Логинимся в WebSocket + useEffect(() => { + if (isConnected && userId) { + const token = localStorage.getItem('access_token'); + if (token) { + login(token); + } + } + }, [isConnected, userId, login]); + + // Освобождаем блокировки при размонтировании + useEffect(() => { + return () => { + releaseAllLocks(); + }; + }, [releaseAllLocks]); + + // Обработчик сохранения ячейки (теперь только для оптимистичного обновления UI) + const handleSaveCell = async (row, column, value) => { + // Обновление данных происходит через WebSocket в Cell компоненте + // Здесь просто обновляем UI для оптимистичного отображения + const updatedData = [...data]; + const rowIndex = data.findIndex(r => (r.id || r.line_id || r.index) === row.id); + if (rowIndex !== -1) { + updatedData[rowIndex][column.id] = value; + setData(updatedData); + } + }; + + // Создаем колонки с передачей userId в Cell const columns = useMemo(() => { if (!columnsConfig || !columnsConfig.columns) return []; + try { - return getTableColumns({ Cell, EditCell, columnsConfig }); + // Оборачиваем Cell компонент для передачи userId + const CellWithProps = (props) => ( + + ); + + return getTableColumns({ + Cell: CellWithProps, + EditCell, + columnsConfig + }); } catch (error) { console.error('Error creating columns:', error); return []; } - }, [columnsConfig]); + }, [columnsConfig, userId, data]); - // Обработчик сохранения ячейки - const handleSaveCell = async (cell, value) => { - const updatedData = [...data]; - updatedData[cell.row.index][cell.column.id] = value; - setData(updatedData); - }; - - // Конфигурация таблицы + // Конфигурация таблицы (без изменений) const tableConfig = useMemo( () => ({ ...BASE_TABLE_CONFIG, columns, data, - // data: [], columnVirtualizerInstanceRef, enableRowVirtualization: false, onColumnSizingChange: setColumnSizing, @@ -100,9 +171,6 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { globalFilter, showColumnFilters, }, - initialState: { - // expanded: true, - }, muiTableHeadCellProps: ({ column }) => ({ sx: { width: column.getSize(), @@ -161,6 +229,34 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { const table = useMaterialReactTable(tableConfig); + // Показываем индикатор загрузки + if (isDataLoading) { + return ( + + + Загрузка данных... + + ); + } + + // Показываем ошибку подключения + if (!isConnected) { + return ( + + + Подключение к серверу... + + ); + } + + if (wsError) { + return ( + + Ошибка подключения: {wsError} + + ); + } + return ( <> { ); }; -export default React.memo(RealtimeTable); +// Основной компонент с провайдером WebSocket +const RealtimeTable = ({ formType, formId, sheetName }) => { + const [direction, setDirection] = useState(null); + + // Получаем direction из URL или пропсов если нужно + useEffect(() => { + // Пример получения direction из URL параметров + const urlParams = new URLSearchParams(window.location.search); + const dir = urlParams.get('direction'); + if (dir) { + setDirection(dir); + } + }, []); + + return ( + + + + ); +}; + +export default React.memo(RealtimeTable); \ No newline at end of file diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js new file mode 100644 index 0000000..800eea8 --- /dev/null +++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js @@ -0,0 +1,176 @@ +import React, { createContext, useContext, useCallback, useRef, useState } from 'react'; +import { useWebSocket } from '../hooks/useWebSocket'; +import { useCellLocks } from '../hooks/useCellLocks'; +import { Config } from '../../../conf/config'; + +const RealtimeContext = createContext(null); + +export const useRealtime = () => { + const context = useContext(RealtimeContext); + if (!context) { + throw new Error('useRealtime must be used within RealtimeProvider'); + } + return context; +}; + +export const RealtimeProvider = ({ children, formId, sheetName, direction, userId }) => { + const [pendingUpdates, setPendingUpdates] = useState(new Map()); + // Формируем URL в соответствии с вашим бэкендом + const wsUrl = `${process.env.REACT_APP_WS_URL || 'ws://localhost:8000'}/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}`; + + const { isConnected, sendMessage, login, error } = useWebSocket(wsUrl, handleMessage); + const { + isCellLocked, + tryLockCell, + unlockCell, + getCellLockInfo, + releaseAllLocks + } = useCellLocks(userId); + + const onCellUpdateRef = useRef(null); + const onRowAddRef = useRef(null); + const onRowDeleteRef = useRef(null); + const onErrorRef = useRef(null); + + function handleMessage(data) { + // Обработка ошибок от бэкенда + if (data.error) { + onErrorRef.current?.(data.error); + return; + } + + // Ваш бэкенд возвращает result после успешной операции + switch (data.event) { + case 'cell_updated': + if (data.result && onCellUpdateRef.current) { + // Ваш бэкенд возвращает результат в виде списка обновленных ячеек + // Формат: [{ line_id, column, value }] + const updatedCells = Array.isArray(data.result) ? data.result : [data.result]; + onCellUpdateRef.current(updatedCells); + } + break; + + case 'row_added': + if (data.result && onRowAddRef.current) { + // Ваш бэкенд возвращает добавленную строку + onRowAddRef.current(data.result); + } + break; + + case 'row_deleted': + if (data.result && onRowDeleteRef.current) { + // Ваш бэкенд возвращает ID удаленной строки + onRowDeleteRef.current(data.result); + } + break; + + case 'user_login': + console.log('User logged in:', data.user_id); + break; + + default: + console.log('Unknown event:', data.event); + } + } + + const updateCell = useCallback(async (rowId, columnId, value) => { + // Пытаемся заблокировать ячейку + if (!tryLockCell(rowId, columnId, userId)) { + const lockInfo = getCellLockInfo(rowId, columnId); + onErrorRef.current?.(`Ячейка редактируется пользователем ${lockInfo?.userId}`); + return false; + } + + // Формируем сообщение в формате вашего бэкенда + const message = { + event: 'cell_updated', + data: { + line_id: rowId, + column: columnId, + value: value + } + }; + + // Сохраняем pending update для оптимистичного UI + const updateKey = `${rowId}_${columnId}`; + setPendingUpdates(prev => new Map(prev).set(updateKey, { value, timestamp: Date.now() })); + + const sent = sendMessage(message); + + if (!sent) { + // Откатываем блокировку если сообщение не отправлено + unlockCell(rowId, columnId, userId); + setPendingUpdates(prev => { + const newMap = new Map(prev); + newMap.delete(updateKey); + return newMap; + }); + return false; + } + + return true; + }, [sendMessage, tryLockCell, unlockCell, getCellLockInfo, userId]); + + const addRow = useCallback(async (rowData) => { + const message = { + event: 'row_added', + data: rowData + }; + + return sendMessage(message); + }, [sendMessage]); + + const deleteRow = useCallback(async (rowId) => { + const message = { + event: 'row_deleted', + data: { row_id: rowId } + }; + + return sendMessage(message); + }, [sendMessage]); + + const subscribeToCellUpdates = useCallback((callback) => { + onCellUpdateRef.current = callback; + return () => { onCellUpdateRef.current = null; }; + }, []); + + const subscribeToRowAdds = useCallback((callback) => { + onRowAddRef.current = callback; + return () => { onRowAddRef.current = null; }; + }, []); + + const subscribeToRowDeletes = useCallback((callback) => { + onRowDeleteRef.current = callback; + return () => { onRowDeleteRef.current = null; }; + }, []); + + const subscribeToErrors = useCallback((callback) => { + onErrorRef.current = callback; + return () => { onErrorRef.current = null; }; + }, []); + + const loginUser = useCallback((token) => { + login(token); + }, [login]); + + return ( + isCellLocked(rowId, columnId, userId), + getCellLockInfo: (rowId, columnId) => getCellLockInfo(rowId, columnId), + tryLockCell: (rowId, columnId) => tryLockCell(rowId, columnId, userId), + subscribeToCellUpdates, + subscribeToRowAdds, + subscribeToRowDeletes, + subscribeToErrors, + login: loginUser, + releaseAllLocks: () => releaseAllLocks(userId) + }}> + {children} + + ); +}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/hooks/useCellLocks.js b/web/src/components/RealtimeTable/hooks/useCellLocks.js new file mode 100644 index 0000000..0a9d659 --- /dev/null +++ b/web/src/components/RealtimeTable/hooks/useCellLocks.js @@ -0,0 +1,122 @@ +import { useState, useCallback, useRef, useEffect } from 'react'; + +export const useCellLocks = (userId, lockTimeout = 30000) => { + const [lockedCells, setLockedCells] = useState(new Map()); + const lockTimersRef = useRef(new Map()); + + const getCellKey = (rowId, columnId) => `${rowId}_${columnId}`; + + const lockCell = useCallback((rowId, columnId, lockingUserId) => { + const cellKey = getCellKey(rowId, columnId); + + setLockedCells(prev => { + const newLocks = new Map(prev); + const existingLock = newLocks.get(cellKey); + + if (existingLock && existingLock.userId !== lockingUserId) { + return prev; // Cell already locked by another user + } + + newLocks.set(cellKey, { + userId: lockingUserId, + lockedAt: Date.now(), + rowId, + columnId + }); + + return newLocks; + }); + + return true; + }, []); + + const unlockCell = useCallback((rowId, columnId, unlockingUserId) => { + const cellKey = getCellKey(rowId, columnId); + + setLockedCells(prev => { + const newLocks = new Map(prev); + const lock = newLocks.get(cellKey); + + if (lock && lock.userId === unlockingUserId) { + newLocks.delete(cellKey); + } + + return newLocks; + }); + + // Clear timer if exists + if (lockTimersRef.current.has(cellKey)) { + clearTimeout(lockTimersRef.current.get(cellKey)); + lockTimersRef.current.delete(cellKey); + } + }, []); + + const tryLockCell = useCallback((rowId, columnId, currentUserId) => { + const cellKey = getCellKey(rowId, columnId); + const existingLock = lockedCells.get(cellKey); + + if (existingLock && existingLock.userId !== currentUserId) { + return false; // Cell is locked by another user + } + + if (!existingLock) { + lockCell(rowId, columnId, currentUserId); + + // Auto-unlock after timeout + const timer = setTimeout(() => { + unlockCell(rowId, columnId, currentUserId); + }, lockTimeout); + + lockTimersRef.current.set(cellKey, timer); + } + + return true; + }, [lockedCells, lockCell, unlockCell, lockTimeout]); + + const releaseAllLocks = useCallback((userId) => { + setLockedCells(prev => { + const newLocks = new Map(prev); + for (const [key, lock] of newLocks.entries()) { + if (lock.userId === userId) { + newLocks.delete(key); + if (lockTimersRef.current.has(key)) { + clearTimeout(lockTimersRef.current.get(key)); + lockTimersRef.current.delete(key); + } + } + } + return newLocks; + }); + }, []); + + const isCellLocked = useCallback((rowId, columnId, currentUserId) => { + const cellKey = getCellKey(rowId, columnId); + const lock = lockedCells.get(cellKey); + return lock && lock.userId !== currentUserId; + }, [lockedCells]); + + const getCellLockInfo = useCallback((rowId, columnId) => { + const cellKey = getCellKey(rowId, columnId); + return lockedCells.get(cellKey); + }, [lockedCells]); + + useEffect(() => { + return () => { + // Clear all timers on unmount + for (const timer of lockTimersRef.current.values()) { + clearTimeout(timer); + } + lockTimersRef.current.clear(); + }; + }, []); + + return { + lockedCells, + lockCell, + unlockCell, + tryLockCell, + isCellLocked, + getCellLockInfo, + releaseAllLocks + }; +}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js new file mode 100644 index 0000000..319e38c --- /dev/null +++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js @@ -0,0 +1,202 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { FormsSheetApi } from "../../../api/form_sheet"; +import { useRealtime } from "../contexts/RealtimeContext"; + +const useRealtimeData = (formId, sheetName) => { + const [data, setData] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const dataRef = useRef(data); + + const { + isConnected: wsConnected, + error: wsError, + subscribeToCellUpdates, + subscribeToRowAdds, + subscribeToRowDeletes, + subscribeToErrors, + } = useRealtime(); + + // Ваша существующая функция форматирования + const formatedToSubRows = useCallback((depth, oldData) => { + if (depth === 0) return oldData; + const dataFiltered = oldData.filter((d) => d.depth < depth); + + for (let i = 0; i < dataFiltered.length; i++) { + const curRow = dataFiltered[i]; + if (curRow.depth !== depth - 1) continue; + const nextRow = dataFiltered[i + 1] || null; + const subRows = oldData.filter( + (d) => + d.depth === depth && + d.sort_order > curRow.sort_order && + (!nextRow || d.sort_order < nextRow.sort_order), + ); + curRow.subRows = subRows; + } + return formatedToSubRows(depth - 1, dataFiltered); + }, []); + + // Загрузка начальных данных + useEffect(() => { + const loadInitialData = async () => { + try { + setIsLoading(true); + const res = await FormsSheetApi.get(formId, sheetName); + const mockData = res.result; + + if (!mockData || mockData.length === 0) { + setData([]); + return; + } + + const maxDepth = mockData.reduce((prev, current) => { + return (prev.depth || 0) > (current.depth || 0) ? prev : current; + }, { depth: 0 }).depth; + + const formattedData = formatedToSubRows(maxDepth, mockData); + setData(formattedData); + } catch (err) { + console.error('Failed to load initial data:', err); + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + loadInitialData(); + }, [formId, sheetName, formatedToSubRows]); + + useEffect(() => { + setIsConnected(wsConnected); + }, [wsConnected]); + + useEffect(() => { + setError(wsError); + }, [wsError]); + + useEffect(() => { + dataRef.current = data; + }, [data]); + + // Подписка на обновления WebSocket + useEffect(() => { + if (!wsConnected) return; + + const unsubscribeCellUpdates = subscribeToCellUpdates((updatedCells) => { + if (!updatedCells || updatedCells.length === 0) return; + + setData(prevData => { + // Функция для обновления данных в иерархической структуре + const updateInHierarchy = (items) => { + return items.map(item => { + let updatedItem = { ...item }; + + // Обновляем ячейки текущего уровня + updatedCells.forEach(cell => { + if ((item.id || item.line_id) === cell.line_id) { + updatedItem[cell.column] = cell.value; + } + }); + + // Рекурсивно обновляем подстроки + if (item.subRows && item.subRows.length > 0) { + updatedItem.subRows = updateInHierarchy(item.subRows); + } + + return updatedItem; + }); + }; + + return updateInHierarchy(prevData); + }); + }); + + const unsubscribeRowAdds = subscribeToRowAdds((newRow) => { + setData(prevData => { + // Преобразуем текущие данные в плоский массив + const flatData = flattenHierarchy(prevData); + + // Добавляем новую строку + const updatedFlatData = [...flatData, newRow]; + + // Сортируем по sort_order + updatedFlatData.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); + + // Восстанавливаем иерархию + const maxDepth = updatedFlatData.reduce((prev, current) => { + return (prev.depth || 0) > (current.depth || 0) ? prev : current; + }, { depth: 0 }).depth; + + return formatedToSubRows(maxDepth, updatedFlatData); + }); + }); + + const unsubscribeRowDeletes = subscribeToRowDeletes((deletedRowId) => { + setData(prevData => { + // Функция для удаления строки из иерархии + const deleteInHierarchy = (items) => { + return items + .filter(item => (item.id || item.line_id) !== deletedRowId) + .map(item => { + if (item.subRows && item.subRows.length > 0) { + return { ...item, subRows: deleteInHierarchy(item.subRows) }; + } + return item; + }); + }; + + return deleteInHierarchy(prevData); + }); + }); + + const unsubscribeErrors = subscribeToErrors((err) => { + console.error('WebSocket error:', err); + setError(err); + setTimeout(() => setError(null), 5000); + }); + + return () => { + unsubscribeCellUpdates(); + unsubscribeRowAdds(); + unsubscribeRowDeletes(); + unsubscribeErrors(); + }; + }, [wsConnected, subscribeToCellUpdates, subscribeToRowAdds, subscribeToRowDeletes, subscribeToErrors, formatedToSubRows]); + + // Вспомогательная функция для преобразования иерархических данных в плоский массив + const flattenHierarchy = useCallback((items, result = []) => { + for (const item of items) { + const { subRows, ...rest } = item; + result.push(rest); + if (subRows && subRows.length > 0) { + flattenHierarchy(subRows, result); + } + } + return result; + }, []); + + const updateData = useCallback((newData) => { + if (Array.isArray(newData) && !newData[0]?.subRows) { + const maxDepth = newData.reduce((prev, current) => { + return (prev.depth || 0) > (current.depth || 0) ? prev : current; + }, { depth: 0 }).depth; + + const formattedData = formatedToSubRows(maxDepth, newData); + setData(formattedData); + } else { + setData(newData); + } + }, [formatedToSubRows]); + + return { + data, + setData: updateData, + isConnected, + isLoading, + error, + }; +}; + +export default useRealtimeData; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/hooks/useWebSocket.js b/web/src/components/RealtimeTable/hooks/useWebSocket.js new file mode 100644 index 0000000..bc82ee5 --- /dev/null +++ b/web/src/components/RealtimeTable/hooks/useWebSocket.js @@ -0,0 +1,114 @@ +import { useEffect, useRef, useCallback, useState } from 'react'; + +export const useWebSocket = (url, onMessage) => { + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(null); + const wsRef = useRef(null); + const reconnectTimeoutRef = useRef(null); + const reconnectAttempts = useRef(0); + + const connect = useCallback(() => { + if (wsRef.current?.readyState === WebSocket.OPEN) return; + + try { + console.log('Connecting to WebSocket:', url); + const ws = new WebSocket(url); + wsRef.current = ws; + + ws.onopen = () => { + console.log('WebSocket connected successfully'); + setIsConnected(true); + setError(null); + reconnectAttempts.current = 0; + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + console.log('WebSocket message received:', data); + onMessage(data); + } catch (err) { + console.error('Failed to parse WebSocket message:', err); + } + }; + + ws.onerror = (event) => { + console.error('WebSocket error:', event); + // Не закрываем соединение при ошибке, просто логируем + setError('Connection error occurred'); + }; + + ws.onclose = (event) => { + console.log('WebSocket closed:', event.code, event.reason); + setIsConnected(false); + + // Не переподключаемся если закрыто intentionally или с ошибкой авторизации + if (event.code === 1008 || event.code === 1000) { + console.log('Connection closed intentionally or auth error, not reconnecting'); + return; + } + + // Reconnect with exponential backoff + if (reconnectAttempts.current < 5) { + const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000); + console.log(`Reconnecting in ${delay}ms...`); + reconnectTimeoutRef.current = setTimeout(() => { + reconnectAttempts.current++; + connect(); + }, delay); + } else { + setError('Max reconnection attempts reached'); + } + }; + } catch (err) { + console.error('Failed to create WebSocket:', err); + setError(err.message); + } + }, [url, onMessage]); + + const disconnect = useCallback(() => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + } + if (wsRef.current) { + wsRef.current.close(1000, 'Intentional disconnect'); + wsRef.current = null; + } + setIsConnected(false); + }, []); + + const sendMessage = useCallback((message) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify(message)); + console.log('Message sent:', message); + return true; + } + console.warn('WebSocket is not connected, readyState:', wsRef.current?.readyState); + setError('Cannot send message: WebSocket not connected'); + return false; + }, []); + + const login = useCallback((token) => { + if (!token) { + console.warn('No token provided for login'); + return false; + } + return sendMessage({ + event: 'user_login', + data: { token } + }); + }, [sendMessage]); + + useEffect(() => { + connect(); + return () => disconnect(); + }, [connect, disconnect]); + + return { + isConnected, + error, + sendMessage, + login, + disconnect + }; +}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/index.jsx b/web/src/components/RealtimeTable/index.jsx index 283141b..33935e0 100644 --- a/web/src/components/RealtimeTable/index.jsx +++ b/web/src/components/RealtimeTable/index.jsx @@ -3,7 +3,7 @@ export { default as RealtimeTable } from './RealtimeTable'; export { default as Cell } from './Cell/Cell/Cell'; export { default as EditCell } from './Cell/EditCell/EditCell'; -export { default as useRealtimeData } from './useRealtimeData'; +export { default as useRealtimeData } from './hooks/useRealtimeData'; export { getTableColumns } from './tableColumns'; // Или можно экспортировать всё вместе diff --git a/web/src/components/RealtimeTable/useRealtimeData.js b/web/src/components/RealtimeTable/useRealtimeData.js deleted file mode 100644 index 79dfa56..0000000 --- a/web/src/components/RealtimeTable/useRealtimeData.js +++ /dev/null @@ -1,488 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { dataMock } from "../../mocks/mockTable"; -import { FormsSheetApi } from "../../api/form_sheet"; - -const useRealtimeData = (formId, sheetName) => { - const [data, setData] = useState([]); - - const formatedToSubRows = useCallback((depth, oldData) => { - if (depth == 0) return oldData; - const data = oldData.filter((d) => d.depth < depth); - - for (let i = 0; i < data.length; i++) { - const curRow = data[i]; - if (curRow.depth !== depth - 1) continue; - const nextRow = data[i + 1] || null; - const subRows = oldData.filter( - (d) => - d.depth == depth && - d.sort_order > curRow.sort_order && - (!nextRow || d.sort_order < nextRow.sort_order), - ); - curRow.subRows = subRows; - } - return formatedToSubRows(depth - 1, data); - }, []); - - useEffect(() => { - const loadInitialData = async () => { - // const res = { - // success: true, - // message: null, - // result: [ - // { - // row_type: "ROOT", - // depth: 0, - // data: { - // q1: { - // total: "1800", - // adj_rf: "2Не применимо", - // pay_ho: "3Не применимо", - // pay_rf: "4Не применимо", - // adj_ssp: "5Не применимо", - // booking: "6Нет данных", - // economy: "7800", - // pay_act: "8Не выполнена", - // pay_date: "91970-01-01", - // actual_m1: "10Нет факта", - // actual_m2: "11Нет факта", - // actual_m3: "12Нет факта", - // pay_amount: "13Не оплачено", - // rem_actual: "14800", - // adj_comment: "15Комментарий отсутствует", - // adj_current: "16Не требуется", - // adj_reserve: "17Не требуется", - // pay_comment: "18Комментарий по оплате отсутствует", - // rem_booking: "19800", - // transfer_q2: "20Нет переноса", - // transfer_q3: "21Нет переноса", - // transfer_q4: "22Нет переноса", - // actual_quarter: "23800", - // corrected_plan: "24800", - // }, - // q2: { - // actual_m1: "actual_m1", - // actual_m2: "actual_m2", - // actual_m3: "actual_m3", - // actual_quarter: "actual_quarter", - // adj_comment: "adj_comment", - // adj_current: "adj_current", - // adj_reserve: "adj_reserve", - // adj_rf: "adj_rf", - // adj_ssp: "adj_ssp", - // booking: "booking", - // corrected_plan: "corrected_plan", - // economy: "economy", - // new_plan: "new_plan", - // pay_act: "pay_act", - // pay_amount: "pay_amount", - // pay_comment: "pay_comment", - // pay_date: "pay_date", - // pay_ho: "pay_ho", - // pay_rf: "pay_rf", - // rem_actual: "rem_actual", - // rem_booking: "rem_booking", - // rev_comment: "rev_comment", - // rev_eco: "rev_eco", - // rev_inc: "rev_inc", - // rev_item: "rev_item", - // rev_seq: "rev_seq", - // total: "total", - // transfer_q3: "transfer_q3", - // transfer_q4: "transfer_q4", - // }, - // q3: { - // total: "800", - // adj_rf: "Не применимо", - // pay_ho: "Не применимо", - // pay_rf: "Не применимо", - // adj_ssp: "Не применимо", - // booking: "Нет данных", - // economy: "800", - // pay_act: "Не выполнена", - // rev_eco: "Нет экономии", - // rev_inc: "Нет увеличения", - // rev_seq: "Нет секвестра", - // new_plan: "800", - // pay_date: "1970-01-01", - // rev_item: "Нет изменений", - // actual_m1: "Нет факта", - // actual_m2: "Нет факта", - // actual_m3: "Нет факта", - // pay_amount: "Не оплачено", - // rem_actual: "800", - // adj_comment: "Комментарий отсутствует", - // adj_current: "Не требуется", - // adj_reserve: "Не требуется", - // pay_comment: "Комментарий по оплате отсутствует", - // rem_booking: "800", - // rev_comment: "Комментарий по пересмотру отсутствует", - // transfer_q4: "Нет переноса", - // actual_quarter: "800", - // corrected_plan: "800", - // }, - // q4: { - // total: "800", - // adj_rf: "Не применимо", - // pay_ho: "Не применимо", - // pay_rf: "Не применимо", - // adj_ssp: "Не применимо", - // booking: "Нет данных", - // economy: "800", - // pay_act: "Не выполнена", - // rev_eco: "Нет экономии", - // rev_inc: "Нет увеличения", - // rev_seq: "Нет секвестра", - // new_plan: "800", - // pay_date: "1970-01-01", - // rev_item: "Нет изменений", - // actual_m1: "Нет факта", - // actual_m2: "Нет факта", - // actual_m3: "Нет факта", - // pay_amount: "Не оплачено", - // rem_actual: "800", - // actual_spod: "Нет данных СПОД", - // adj_comment: "Комментарий отсутствует", - // adj_current: "Не требуется", - // adj_reserve: "Не требуется", - // pay_comment: "Комментарий по оплате отсутствует", - // rem_booking: "800", - // rev_comment: "Комментарий по пересмотру отсутствует", - // actual_quarter: "800", - // corrected_plan: "800", - // }, - // ckk: { - // q1: "Нет плана", - // q2: "Нет плана", - // q3: "Нет плана", - // q4: "Нет плана", - // ceiling: "Лимит не установлен", - // comment: "Комментарий по ЦКК отсутствует", - // deadline: "1970-01-01", - // proc_plan: "Нет плана процедур", - // proc_method: "Не определен", - // rf_schedule: "График не задан", - // }, - // plan: { - // q1: "Нет плана", - // q2: "Нет плана", - // q3: "Нет плана", - // q4: "Нет плана", - // year: "800", - // comment: "Комментарий по плану отсутствует", - // }, - // header: { - // name: "Прочие операционные расходы", - // year: 2026, - // item_id: "Нет ID", - // section: "4.00.0.", - // num_group: "Номер группы не задан", - // internal_order: "Нет внутреннего заказа", - // expense_item_id: 708, - // }, - // totals: { - // pay_year: "Оплата за год не производилась", - // fact_year: "800", - // }, - // line_id: "Нет ID строки", - // reserve: { - // q1: "Нет резерва", - // q2: "Нет резерва", - // q3: "Нет резерва", - // q4: "Нет резерва", - // year: "800", - // justification: "Обоснование резерва отсутствует", - // }, - // approved: { - // q1: "800", - // q2: "800", - // q3: "800", - // q4: "800", - // year: "800", - // }, - // collegial: { - // note: "Замечания коллегиального органа отсутствуют", - // approved: "Не утверждено", - // protocol: "Нет протокола", - // }, - // allocation: { - // order: "Приказ не указан", - // property: "Свойства распределения не заданы", - // }, - // sequestration: { - // q1: "Нет секвестра", - // q2: "Нет секвестра", - // q3: "Нет секвестра", - // q4: "Нет секвестра", - // year: "800", - // justification: "Обоснование секвестра отсутствует", - // }, - // contract_detail: { - // q1: "Нет деталей", - // q2: "Нет деталей", - // q3: "Нет деталей", - // q4: "Нет деталей", - // act: "Акт не предоставлен", - // addenda: "Нет доп. соглашений", - // ceiling: "Потолок не установлен", - // comment: "Комментарий по контракту отсутствует", - // subject: "Предмет контракта не указан", - // currency: "RUB", - // deadline: "1970-01-01", - // vat_rate: "Нет ставки НДС", - // reference: "Ссылка отсутствует", - // rf_schedule: "График не задан", - // counterparty: "Контрагент не определен", - // exchange_rate: -1, - // amount_foreign: -1, - // payment_scheme: "Схема оплаты не определена", - // }, - // contract_summary: { - // total: "Итог по контракту не подведен", - // comment: "Комментарий по сводке контракта отсутствует", - // deadline: "1970-01-01", - // future_y1: "Нет данных на след. год", - // future_y2: "Нет данных через год", - // other_ssp: "Нет данных по СПП", - // counterparty: "Контрагент не определен", - // }, - // }, - // sort_order: 1, - // }, - // ], - // count: 38, - // }; - - // const res = { - // success: true, - // message: null, - // result: [ - // { - // row_type: "ROOT", - // depth: 0, - // data: { - // q1: { - // total: 0, - // adj_ssp: "adj_ssp_mock", - // booking: "booking_mock", - // pay_act: "pay_act_mock", - // pay_date: "2025-01-15", - // actual_m1: "1000.00", - // actual_m2: "1200.00", - // actual_m3: "1100.00", - // pay_amount: "3300.00", - // adj_comment: "adj_comment_mock", - // adj_current: "adj_current_mock", - // adj_reserve: "adj_reserve_mock", - // pay_comment: "pay_comment_mock", - // transfer_q2: "transfer_q2_mock", - // transfer_q3: "transfer_q3_mock", - // transfer_q4: "transfer_q4_mock", - // transfer_econ: "transfer_econ_mock", - // actual_quarter: 3300, - // corrected_plan: 100, - // transfer_far_comment: "transfer_far_comment_mock", - // residual_after_actual: 100, - // transfer_next_comment: "transfer_next_comment_mock", - // residual_after_booking: 100, - // transfer_q2_delay_acts: "transfer_q2_delay_acts_mock", - // transfer_q2_economy_rf: "transfer_q2_economy_rf_mock", - // transfer_q2_delay_procurement: - // "transfer_q2_delay_procurement_mock", - // }, - // q2: { - // total: 0, - // adj_ssp: "adj_ssp_mock_q2", - // booking: "booking_mock_q2", - // pay_act: "pay_act_mock_q2", - // new_plan: 200, - // pay_date: "2025-04-20", - // actual_m1: "2000.00", - // actual_m2: "2100.00", - // actual_m3: "1900.00", - // pay_amount: "6000.00", - // adj_comment: "adj_comment_mock_q2", - // adj_current: "adj_current_mock_q2", - // adj_reserve: "adj_reserve_mock_q2", - // pay_comment: "pay_comment_mock_q2", - // transfer_q3: "transfer_q3_mock", - // transfer_q4: "transfer_q4_mock", - // revision_inc: "revision_inc_mock", - // revision_seq: "revision_seq_mock", - // target_change: "target_change_mock", - // transfer_econ: "transfer_econ_mock_q2", - // actual_quarter: 6000, - // corrected_plan: 200, - // base_correction: "base_correction_mock", - // revision_comment: "revision_comment_mock", - // transfer_far_comment: "transfer_far_comment_mock_q2", - // residual_after_actual: 200, - // transfer_next_comment: "transfer_next_comment_mock_q2", - // residual_after_booking: 200, - // transfer_q3_delay_acts: "transfer_q3_delay_acts_mock", - // transfer_q3_economy_rf: "transfer_q3_economy_rf_mock", - // base_correction_comment: "base_correction_comment_mock", - // transfer_q3_delay_procurement: - // "transfer_q3_delay_procurement_mock", - // }, - // q3: { - // total: 0, - // adj_ssp: "adj_ssp_mock_q3", - // booking: "booking_mock_q3", - // pay_act: "pay_act_mock_q3", - // new_plan: 100, - // pay_date: "2025-07-10", - // actual_m1: "900.00", - // actual_m2: "850.00", - // actual_m3: "950.00", - // pay_amount: "2700.00", - // adj_comment: "adj_comment_mock_q3", - // adj_current: "adj_current_mock_q3", - // adj_reserve: "adj_reserve_mock_q3", - // pay_comment: "pay_comment_mock_q3", - // transfer_q4: "transfer_q4_mock_q3", - // revision_inc: "revision_inc_mock_q3", - // revision_seq: "revision_seq_mock_q3", - // target_change: "target_change_mock_q3", - // transfer_econ: "transfer_econ_mock_q3", - // actual_quarter: 2700, - // corrected_plan: 100, - // base_correction: "base_correction_mock_q3", - // revision_comment: "revision_comment_mock_q3", - // residual_after_actual: 100, - // transfer_next_comment: "transfer_next_comment_mock_q3", - // residual_after_booking: 100, - // transfer_q4_delay_acts: "transfer_q4_delay_acts_mock", - // transfer_q4_economy_rf: "transfer_q4_economy_rf_mock", - // base_correction_comment: "base_correction_comment_mock_q3", - // transfer_q4_delay_procurement: - // "transfer_q4_delay_procurement_mock", - // }, - // q4: { - // total: "total_mock_q4", - // adj_ssp: "adj_ssp_mock_q4", - // booking: "booking_mock_q4", - // pay_act: "pay_act_mock_q4", - // new_plan: 200, - // pay_date: "2025-10-05", - // actual_m1: "1500.00", - // actual_m2: "1600.00", - // actual_m3: "1400.00", - // pay_amount: "4500.00", - // actual_spod: "actual_spod_mock", - // adj_comment: "adj_comment_mock_q4", - // adj_current: "adj_current_mock_q4", - // adj_reserve: "adj_reserve_mock_q4", - // pay_comment: "pay_comment_mock_q4", - // revision_inc: "revision_inc_mock_q4", - // revision_seq: "revision_seq_mock_q4", - // target_change: "target_change_mock_q4", - // transfer_econ: "transfer_econ_mock_q4", - // actual_quarter: 4500, - // corrected_plan: 200, - // base_correction: "base_correction_mock_q4", - // revision_comment: "revision_comment_mock_q4", - // residual_after_actual: 200, - // residual_after_booking: 200, - // base_correction_comment: "base_correction_comment_mock_q4", - // }, - // plan: { - // q1: "plan_q1_mock", - // q2: "plan_q2_mock", - // q3: "plan_q3_mock", - // q4: "plan_q4_mock", - // year: 2025, - // comment: "plan_comment_mock", - // }, - // header: { - // name: "Административно-хозяйственные расходы", - // vsp_id: "VSP_001", - // item_id: "ITEM_001", - // num_group: "GRP_001", - // vsp_address: "г. Москва, ул. Примерная, д. 1", - // section_code: "1.00.0.", - // expense_item_id: 1, - // }, - // totals: { - // pay_year: 16500, - // fact_year: 16500, - // economy_year: 200, - // }, - // booking: { - // y2026: { - // q1: "booking_y2026_q1_mock", - // q2: "booking_y2026_q2_mock", - // q3: "booking_y2026_q3_mock", - // q4: "booking_y2026_q4_mock", - // }, - // y2027: { - // q1: "booking_y2027_q1_mock", - // q2: "booking_y2027_q2_mock", - // q3: "booking_y2027_q3_mock", - // q4: "booking_y2027_q4_mock", - // }, - // }, - // line_id: "LINE_001", - // seq_ssp: { - // q1: "seq_ssp_q1_mock", - // q2: "seq_ssp_q2_mock", - // q3: "seq_ssp_q3_mock", - // q4: "seq_ssp_q4_mock", - // year: 600, - // justification: "seq_ssp_justification_mock", - // }, - // approved: { - // q1: 100, - // q2: 200, - // q3: 100, - // q4: 200, - // year: 600, - // }, - // contract: { - // act: "contract_act_mock", - // date: "2025-01-01", - // scheme: "contract_scheme_mock", - // ceiling: "1000000.00", - // comment: "contract_comment_mock", - // subject: "contract_subject_mock", - // currency: "RUB", - // deadline: "2025-12-31", - // vat_rate: "20%", - // reference: "contract_ref_001", - // counterparty: "ООО Контрагент", - // }, - // seq_dfip: { - // q1: 100, - // q2: 200, - // q3: 100, - // q4: 200, - // year: 600, - // justification: "seq_dfip_justification_mock", - // }, - // }, - // sort_order: 1, - // }, - // ], - // count: 1, - // }; - const res = await FormsSheetApi.get(formId, sheetName); - const mockData = res.result; - const maxDepth = mockData.reduce((prev, current) => { - return prev.depth > current.depth ? prev : current; - }).depth; - - const data = formatedToSubRows(maxDepth, mockData); - - setData(data); - }; - - loadInitialData(); - }, [formatedToSubRows]); - - return { - data, - setData, - }; -}; - -export default useRealtimeData; diff --git a/web/src/pages/AdminPanel/UsersPage/UsersPage.jsx b/web/src/pages/AdminPanel/UsersPage/UsersPage.jsx index 98757e2..e9fc198 100644 --- a/web/src/pages/AdminPanel/UsersPage/UsersPage.jsx +++ b/web/src/pages/AdminPanel/UsersPage/UsersPage.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { PageContainer } from '../../StyledForPage'; import { HeaderSwitch } from '../../../components/HeaderMenu/HeaderSwitch'; import { SwitchUserSsp } from '../../../components/AdminPanel/SwitchUserSsp'; -import { ROLES_NAME_ID } from '../../../constants'; +import { ROLES_ID_RUSSIAN_NAME_ARRAY, ROLES_NAME_ID } from '../../../constants'; import { useAuth } from '../../../app/context/AuthProvider'; import { Box, TextField, Autocomplete, FormLabel } from '@mui/material'; import SearchComponent from '../../../components/common/SearchComponent'; @@ -33,7 +33,6 @@ const UsersPage = () => { const formatUsers = async (users) => { const userPromises = users.map(async (user) => { - // if (user.role_id == ROLES_NAME_ID['executor_dfip']) { try { const data = await SspApi.getManySsp(user.id); if (!data.success) { @@ -44,7 +43,6 @@ const UsersPage = () => { } catch (error) { toast.error('Ошибка получения пользователей'); } - // } user.ssps_data = user.ssp .map((sspId) => deps.find((d) => d.id === sspId) || null) @@ -67,13 +65,9 @@ const UsersPage = () => { const activeDeps = data.result.filter((v) => v.is_active); setDeps(activeDeps); }); - UsersApi.getAllRoles().then((data) => { - if (!data.success) { - toast.error('Ошибка получения Ролей ' + data.message); - return; - } - setRoles(data.result); - }); + setRoles(ROLES_ID_RUSSIAN_NAME_ARRAY); + + }, []); useEffect(() => { @@ -214,7 +208,7 @@ const UsersPage = () => { update: [sspId], delete: user.role_id === ROLES_NAME_ID.executor_rf && - user.ssp[0] !== undefined + user.ssp[0] !== undefined ? [user.ssp[0]] : [], }; @@ -229,12 +223,13 @@ const UsersPage = () => { }; if (!user || user.role_id != ROLES_NAME_ID.admin) return; + return ( <> - - - + + + { - + { onClose={handleCloseModalAddUsers} sspOptions={deps} onAddUser={handleAddUserToList} - > + /> ); }; -export default UsersPage; +export default UsersPage; \ No newline at end of file