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