Compare commits

..

No commits in common. "8c1341bba8c2deefc056f6312601c7c50c5f9b4e" and "e916f05ff2545204b92354fa40f6b6a2bd3a1d54" have entirely different histories.

8 changed files with 130 additions and 262 deletions

View File

@ -31,15 +31,6 @@ const INVALID_STYLES = {
color: '#C60C0C', color: '#C60C0C',
}; };
const EDITABLE_STYLES = {
cursor: 'pointer',
border: '2px solid #d8e4ea94',
};
const NOT_EDITABLE_STYLES = {
cursor: 'default',
};
const LOCK_BADGE_STYLES = { const LOCK_BADGE_STYLES = {
position: 'absolute', position: 'absolute',
top: '2px', top: '2px',
@ -186,9 +177,8 @@ const CellComponent = ({
color: textColor, color: textColor,
...(isLocked ? LOCKED_STYLES : {}), ...(isLocked ? LOCKED_STYLES : {}),
...(isInvalid ? INVALID_STYLES : {}), ...(isInvalid ? INVALID_STYLES : {}),
...(isEditable ? EDITABLE_STYLES : NOT_EDITABLE_STYLES),
}), }),
[backgroundColor, isInvalid, isLocked, textColor, isEditable], [backgroundColor, isInvalid, isLocked, textColor],
); );
const lockBadge = useMemo(() => { const lockBadge = useMemo(() => {
@ -197,10 +187,10 @@ const CellComponent = ({
}, [isLocked]); }, [isLocked]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (!isLocked && isEditable && onClick) { if (!isLocked && onClick) {
onClick(); onClick();
} }
}, [isLocked, isEditable, onClick]); }, [isLocked, onClick]);
return ( return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> // biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
@ -227,4 +217,4 @@ const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
); );
}); });
export default Cell; export default Cell;

View File

@ -34,7 +34,7 @@ import { debounce } from '@mui/material';
import { CreateProgramModal } from './Modals/CreateProgramProjectModal'; import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const { data, setData, columnsCurStage, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
const [globalFilter, setGlobalFilter] = useState(''); const [globalFilter, setGlobalFilter] = useState('');
const [showColumnFilters, setShowColumnFilters] = useState(false); const [showColumnFilters, setShowColumnFilters] = useState(false);
const [selectedColumnId, setSelectedColumnId] = useState(); const [selectedColumnId, setSelectedColumnId] = useState();
@ -48,9 +48,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
columnsConfig?.columns, columnsConfig?.columns,
); );
const [expanded, setExpanded] = useState(true);
const [showOnlyDepth2, setShowOnlyDepth2] = useState(false);
const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false); const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
const [isOpenModalCreateProgram, setIsOpenModalCreateProgram] = useState(false); const [isOpenModalCreateProgram, setIsOpenModalCreateProgram] = useState(false);
@ -177,13 +174,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
}, []); }, []);
const columns = useMemo(() => { const columns = useMemo(() => {
if (!columnsConfig || !columnsConfig.columns || columnsCurStage.length === 0) return []; if (!columnsConfig || !columnsConfig.columns) return [];
try { try {
return getTableColumns({ return getTableColumns({
Cell, Cell,
EditCell, EditCell,
columnsConfig, columnsConfig,
columnsCurStage,
onCellUpdate: handleUpdateCell, onCellUpdate: handleUpdateCell,
onCellNumberClick: handleClickRowCell, onCellNumberClick: handleClickRowCell,
onCellUpdateError: handleCellUpdateError, onCellUpdateError: handleCellUpdateError,
@ -195,7 +191,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
} }
}, [ }, [
columnsConfig, columnsConfig,
columnsCurStage,
handleCellUpdateError, handleCellUpdateError,
handleClickRowCell, handleClickRowCell,
handleUpdateCell, handleUpdateCell,
@ -227,7 +222,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
columnSizing, columnSizing,
columnPinning, columnPinning,
columnVisibility, columnVisibility,
expanded,
globalFilter, globalFilter,
showColumnFilters, showColumnFilters,
rowSelection, rowSelection,
@ -235,7 +229,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
tableMeta, tableMeta,
setColumnSizing, setColumnSizing,
setColumnPinning, setColumnPinning,
setExpanded,
editingCell, editingCell,
contextStartEditing, contextStartEditing,
}) => ({ }) => ({
@ -270,7 +263,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
onColumnSizingChange: setColumnSizing, onColumnSizingChange: setColumnSizing,
onColumnPinningChange: setColumnPinning, onColumnPinningChange: setColumnPinning,
onExpandedChange: setExpanded,
onGlobalFilterChange: handleGlobalFilterChange, onGlobalFilterChange: handleGlobalFilterChange,
state: { state: {
columnSizing, columnSizing,
@ -280,7 +272,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
showColumnFilters, showColumnFilters,
rowSelection, rowSelection,
editingCell, editingCell,
expanded
}, },
initialState: { initialState: {
expanded: true, expanded: true,
@ -321,13 +312,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
}, },
}); });
// Конфигурация таблицы
const tableConfig = useMemo(() => createTableConfig({ const tableConfig = useMemo(() => createTableConfig({
columns, columns,
data, data,
columnSizing, columnSizing,
columnPinning, columnPinning,
columnVisibility, columnVisibility,
expanded,
globalFilter, globalFilter,
showColumnFilters, showColumnFilters,
rowSelection, rowSelection,
@ -335,7 +326,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
tableMeta, tableMeta,
setColumnSizing, setColumnSizing,
setColumnPinning, setColumnPinning,
setExpanded,
editingCell, editingCell,
contextStartEditing, contextStartEditing,
}), [ }), [
@ -352,7 +342,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setColumnSizing, setColumnSizing,
setColumnPinning, setColumnPinning,
editingCell, editingCell,
expanded,
]); ]);
const table = useMaterialReactTable(tableConfig); const table = useMaterialReactTable(tableConfig);
@ -391,6 +380,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
} }
if (editingCell) return; if (editingCell) return;
// Отложить поиск до следующего тика
requestAnimationFrame(() => { requestAnimationFrame(() => {
try { try {
const row = table.getRow(dataEditingCells.line_id); const row = table.getRow(dataEditingCells.line_id);
@ -491,12 +481,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
return handleOpenModalSelectExpenseItem(); return handleOpenModalSelectExpenseItem();
} }
return handleAddRow(); return handleAddRow();
}, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]); }, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow]);
const addProgram = useCallback(() => { const addProgram = useCallback(() => {
setIsProgramCreate(true); setIsProgramCreate(true);
setIsOpenModalCreateProgram(true); setIsOpenModalCreateProgram(true);
}, []) }, [setIsOpenModalCreateProgram])
const addProject = useCallback(() => { const addProject = useCallback(() => {
setIsProgramCreate(false); setIsProgramCreate(false);
@ -526,32 +516,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
}; };
}, []); }, []);
const handleToggleDepthVisibility = useCallback(() => {
setShowOnlyDepth2(prev => {
const newState = !prev;
if (newState) {
const allRows = table.getRowModel().flatRows;
console.log(allRows);
const newExpanded = {};
for (const row of allRows) {
const depth = row.original.depth || 0;
if (depth < 2 && row.getCanExpand()) {
newExpanded[row.id] = true;
}
}
setExpanded(newExpanded);
} else {
setExpanded(true);
}
return newState;
});
}, [table]);
return ( return (
<> <>
<SettingsPanel <SettingsPanel
@ -563,7 +527,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
onUnpinColumn={handleUnpinColumn} onUnpinColumn={handleUnpinColumn}
onToggleColumnVisibility={handleToggleColumnVisibility} onToggleColumnVisibility={handleToggleColumnVisibility}
columnVisibility={columnVisibility} columnVisibility={columnVisibility}
columnsCurStage={columnsCurStage}
onChangeSizeMult={setSizeMult} onChangeSizeMult={setSizeMult}
onGlobalFilterChange={handleGlobalFilterChange} onGlobalFilterChange={handleGlobalFilterChange}
sizeMult={sizeMult} sizeMult={sizeMult}
@ -577,12 +540,10 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
sheetName={sheetName} sheetName={sheetName}
direction={direction} direction={direction}
year={year} year={year}
isProject={formType === 'PROJECT'} isProject={formType == 'PROJECT'}
formType={formType} formType={formType}
validationErrors={validationErrors} validationErrors={validationErrors}
onNavigateToColumn={handleNavigateToColumn} onNavigateToColumn={handleNavigateToColumn}
showOnlyDepth2={showOnlyDepth2}
onToggleDepthVisibility={handleToggleDepthVisibility}
/> />
<div style={tableWrapperStyle}> <div style={tableWrapperStyle}>

View File

@ -1,17 +1,15 @@
import { Divider, IconButton, Stack, Tooltip } from '@mui/material'; import { Divider, IconButton, Stack, Tooltip } from '@mui/material';
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState } from 'react';
import { FORM_TYPE_OPTIONS } from '../../../constants/constants'; import { FORM_TYPE_OPTIONS } from '../../../constants/constants';
import { exportSheet, exportSheetProject } from '../../../utils/exportFile'; import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions'; import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
import CollapsibleTree from '../../common/CollapsibleTree'; import CollapsibleTree from '../../common/CollapsibleTree';
import SearchComponent from '../../common/SearchComponent'; import SearchComponent from '../../common/SearchComponent';
import { AddLine, Eye, Minus, Pin, Plus, Program, Project, RemoveLine, Search } from '../../common/icons/icons'; import { AddLine, Minus, Pin, Plus, Program, Project, RemoveLine, Search } from '../../common/icons/icons';
import { GroupByObject } from './GroupByObject/GroupByObject'; import { GroupByObject } from './GroupByObject/GroupByObject';
import { Panel } from './SettingPanel.style'; import { Panel } from './SettingPanel.style';
import ValidationErrorsAlert from './ValidationErrorsAlert'; import ValidationErrorsAlert from './ValidationErrorsAlert';
import ZoomSlider from './ZoomSlider'; import ZoomSlider from './ZoomSlider';
import { collectLeafColumns } from '../utils/columnUtils';
import { stageColumnsHiddenFromPicker } from '../../Stages/constants';
const SettingPanel = ({ const SettingPanel = ({
selectedColumnId, selectedColumnId,
@ -22,7 +20,6 @@ const SettingPanel = ({
onUnpinColumn, onUnpinColumn,
onToggleColumnVisibility, onToggleColumnVisibility,
columnVisibility, columnVisibility,
columnsCurStage,
onChangeSizeMult, onChangeSizeMult,
onGlobalFilterChange, onGlobalFilterChange,
sizeMult, sizeMult,
@ -40,8 +37,6 @@ const SettingPanel = ({
formType, formType,
validationErrors = [], validationErrors = [],
onNavigateToColumn, onNavigateToColumn,
showOnlyDepth2,
onToggleDepthVisibility,
}) => { }) => {
const [isPinned, setIsPinned] = useState(false); const [isPinned, setIsPinned] = useState(false);
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
@ -86,20 +81,6 @@ const SettingPanel = ({
await exportSheet(formId, sheetName, direction); await exportSheet(formId, sheetName, direction);
setIsExporting(false); setIsExporting(false);
}; };
// Обработчик скрытия всех столбцов кроме текущего этапа
const handleShowOnlyCurrentStageColumns = useCallback(() => {
if (!columnsCurStage || columnsCurStage.length === 0) {
console.warn('Нет столбцов для текущего этапа');
return;
}
//колонки которые не относятся не к какому этапу
const allColumnVisible = [...stageColumnsHiddenFromPicker, ...columnsCurStage];
const leafCols = collectLeafColumns({ columns: columns });
for (const col of leafCols) {
onToggleColumnVisibility(col, allColumnVisible.includes(col.id));
}
}, [columnsCurStage, columns, columnVisibility, onToggleColumnVisibility]);
return ( return (
<> <>
@ -144,7 +125,7 @@ const SettingPanel = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{/* проверяем на 4 форму */} {/* проверяем на 4 форму */}
{formType === FORM_TYPE_OPTIONS[3] && ( {formType == FORM_TYPE_OPTIONS[3] && (
<> <>
<Tooltip title='Добавить программу'> <Tooltip title='Добавить программу'>
<IconButton onClick={onAddProgram} variant='outlined'> <IconButton onClick={onAddProgram} variant='outlined'>
@ -161,11 +142,6 @@ const SettingPanel = ({
</Tooltip> </Tooltip>
</> </>
)} )}
<Tooltip title='Скрыть статьи расходов'>
<IconButton onClick={onToggleDepthVisibility} variant='outlined'>
<Eye />
</IconButton>
</Tooltip>
</GroupByObject> </GroupByObject>
</> </>
)} )}
@ -176,16 +152,6 @@ const SettingPanel = ({
) : ( ) : (
'' ''
)} )}
<Tooltip title='Скрыть все столбцы кроме текущих'>
<IconButton
variant='outlined'
color='success'
onClick={handleShowOnlyCurrentStageColumns}
disabled={!columnsCurStage || columnsCurStage.length === 0}
>
<Eye />
</IconButton>
</Tooltip>
</GroupByObject> </GroupByObject>
<Divider orientation='vertical' flexItem /> <Divider orientation='vertical' flexItem />
<ZoomSlider onChange={onChangeSizeMult} curScale={sizeMult} /> <ZoomSlider onChange={onChangeSizeMult} curScale={sizeMult} />
@ -215,4 +181,4 @@ const SettingPanel = ({
); );
}; };
export default SettingPanel; export default SettingPanel;

View File

@ -1,62 +1,62 @@
export const greenColumn = { export const greenColumn = {
ROOT: "#C2D59A", ROOT: '#C2D59A',
GROUP: "#D7E3BC", GROUP: '#D7E3BC',
ITEM: "#EAF0DD", ITEM: '#EAF0DD',
SUB_ITEM: "#EAF0DD", SUB_ITEM: '#FFFFFF',
INPUT: "#FFFFFF", INPUT: '#FFFFFF',
COLOR: "#000000", COLOR: '#000000',
}; };
export const orangeColumn = { export const orangeColumn = {
ROOT: "#F1C297", ROOT: '#F1C297',
GROUP: "#F6D6B9", GROUP: '#F6D6B9',
ITEM: "#FAEBDC", ITEM: '#FAEBDC',
SUB_ITEM: "#FAEBDC", SUB_ITEM: '#FFFFFF',
INPUT: "#FFFFFF", INPUT: '#FFFFFF',
COLOR: "#000000", COLOR: '#000000',
}; };
export const whiteColumn = { export const whiteColumn = {
ROOT: "#FFFFFF", ROOT: '#FFFFFF',
GROUP: "#FFFFFF", GROUP: '#FFFFFF',
ITEM: "#ffffff", ITEM: '#ffffff',
SUB_ITEM: "#ffffff", SUB_ITEM: '#FFFFFF',
INPUT: "#FFFFFF", INPUT: '#FFFFFF',
COLOR: "#000000", COLOR: '#000000',
}; };
export const redColumn = { export const redColumn = {
ROOT: "#933634", ROOT: '#933634', // самый темный
GROUP: "#D89493", GROUP: '#D89493', // средний
ITEM: "#E5B7B6", ITEM: '#E5B7B6', // самый светлый
SUB_ITEM: "#E5B7B6", SUB_ITEM: '#FFFFFF',
INPUT: "#FFFFFF", INPUT: '#FFFFFF',
COLOR: "#ffffff", COLOR: '#ffffff',
}; };
export const blueColumn = { export const blueColumn = {
ROOT: "#538DD4", ROOT: '#538DD4', // самый темный
GROUP: "#8DB3E2", GROUP: '#8DB3E2', // средний
ITEM: "#C5D8F0", ITEM: '#C5D8F0', // самый светлый
SUB_ITEM: "#C5D8F0", SUB_ITEM: '#FFFFFF',
INPUT: "#FFFFFF", INPUT: '#FFFFFF',
COLOR: "#000000", COLOR: '#000000',
}; };
export const tealColumn = { export const tealColumn = {
ROOT: "#92CDDC", ROOT: '#92CDDC', // самый темный
GROUP: "#B6DDE8", GROUP: '#B6DDE8', // средний
ITEM: "rgb(211, 228, 248)", ITEM: 'rgb(211, 228, 248)', // самый светлый
SUB_ITEM: "rgb(211, 228, 248)", SUB_ITEM: '#FFFFFF',
INPUT: "#FFFFFF", INPUT: '#FFFFFF',
COLOR: "#000000", COLOR: '#000000',
}; };
export const yellowColumn = { export const yellowColumn = {
ROOT: "#FFFF66", ROOT: '#FFFF66',
GROUP: "#FFFF99", GROUP: '#FFFF99',
ITEM: "#FFFFCC", ITEM: '#FFFFCC',
SUB_ITEM: "#FFFFCC", SUB_ITEM: '#FFFFFF',
INPUT: "#FFFFFF", INPUT: '#FFFFFF',
COLOR: "#000000", COLOR: '#000000',
}; };

View File

@ -1,45 +1,28 @@
import { import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
createContext, import { toast } from 'react-toastify';
useCallback, import { useWebSocket } from '../hooks/useWebSocket';
useContext, import { getRowId } from '../utils/rowUtils';
useEffect,
useRef,
useState,
} from "react";
import { toast } from "react-toastify";
import { useWebSocket } from "../hooks/useWebSocket";
import { getRowId } from "../utils/rowUtils";
const RealtimeContext = createContext(null); const RealtimeContext = createContext(null);
export const useRealtime = () => { export const useRealtime = () => {
const context = useContext(RealtimeContext); const context = useContext(RealtimeContext);
if (!context) { if (!context) {
throw new Error("useRealtime must be used within RealtimeProvider"); throw new Error('useRealtime must be used within RealtimeProvider');
} }
return context; return context;
}; };
export const RealtimeProvider = ({ export const RealtimeProvider = ({ children, formId, sheetName, direction, year, userId, isProject = false }) => {
children,
formId,
sheetName,
direction,
year,
userId,
isProject = false,
}) => {
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH) // Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
const wsUrl = `${( const wsUrl =
process.env.REACT_APP_API_URL || `${(process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || 'ws://localhost:8000').replace(
process.env.REACT_APP_API_URL || /^http/,
process.env.REACT_APP_ROOT_PATH || 'ws',
"ws://localhost:8000" )}` +
).replace(/^http/, "ws")}${ (!isProject
!isProject ? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}`
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}` : `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`);
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`
}`;
//для ячеек которые редактируются другими пользователями //для ячеек которые редактируются другими пользователями
const [lockedCells, setLockedCells] = useState([]); const [lockedCells, setLockedCells] = useState([]);
@ -47,68 +30,65 @@ export const RealtimeProvider = ({
// Обработка ошибок // Обработка ошибок
if (data.error) { if (data.error) {
console.log(data.error.message); console.log(data.error.message);
toast.error(`Server error:${data.error.message}` || ""); toast.error('Server error:' + data.error.message || '');
onErrorRef.current?.(data.error); onErrorRef.current?.(data.error);
return; return;
} }
switch (data.event) { switch (data.event) {
case "cell_edit_start": case 'cell_edit_start':
console.log("Cell edit started:", data); console.log('Cell edit started:', data);
if (onCellEditStartRef.current) { if (onCellEditStartRef.current) {
onCellEditStartRef.current(data); onCellEditStartRef.current(data);
} }
break; break;
case "cell_edit_end": case 'cell_edit_end':
console.log("Cell edit ended:", data); console.log('Cell edit ended:', data);
if (onCellEditEndRef.current) { if (onCellEditEndRef.current) {
onCellEditEndRef.current(data); onCellEditEndRef.current(data);
} }
break; break;
case "cell_updated": case 'cell_updated':
if (data.result && onCellUpdateRef.current) { if (data.result && onCellUpdateRef.current) {
const updatedCells = Array.isArray(data.result) const updatedCells = Array.isArray(data.result) ? data.result : [data.result];
? data.result
: [data.result];
onCellUpdateRef.current(updatedCells); onCellUpdateRef.current(updatedCells);
} }
break; break;
case "row_added": case 'row_added':
if (data.result && onRowAddRef.current) { if (data.result && onRowAddRef.current) {
onRowAddRef.current(data); onRowAddRef.current(data);
} }
break; break;
case "row_deleted": case 'row_deleted':
if (data.result && onRowDeleteRef.current) { if (data.result && onRowDeleteRef.current) {
onRowDeleteRef.current(data); onRowDeleteRef.current(data);
} }
break; break;
case "program_added": case 'program_added':
console.log("Program added:", data); console.log('Program added:', data);
if (data.result && onProgramAddRef.current) { if (data.result && onProgramAddRef.current) {
onProgramAddRef.current(data); onProgramAddRef.current(data);
} }
break; break;
case "project_added": case 'project_added':
console.log("Project added:", data); console.log('Project added:', data);
if (data.result && onProjectAddRef.current) { if (data.result && onProjectAddRef.current) {
onProjectAddRef.current(data); onProjectAddRef.current(data);
} }
break; break;
default: default:
console.log("Unknown event:", data.event); console.log('Unknown event:', data.event);
} }
}, []); }, []);
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket(wsUrl, handleMessage);
useWebSocket(wsUrl, handleMessage);
const onCellUpdateRef = useRef(null); const onCellUpdateRef = useRef(null);
const onRowAddRef = useRef(null); const onRowAddRef = useRef(null);
@ -125,16 +105,16 @@ export const RealtimeProvider = ({
// Функция для получения токена из localStorage // Функция для получения токена из localStorage
const getAccessToken = useCallback(() => { const getAccessToken = useCallback(() => {
try { try {
const token = localStorage.getItem("access_token"); const token = localStorage.getItem('access_token');
if (!token) { if (!token) {
console.warn("No access token found in localStorage"); console.warn('No access token found in localStorage');
return null; return null;
} }
return token; return token;
} catch (error) { } catch (error) {
console.error("Error reading token from localStorage:", error); console.error('Error reading token from localStorage:', error);
return null; return null;
} }
}, []); }, []);
@ -142,13 +122,13 @@ export const RealtimeProvider = ({
const sendLogin = useCallback(() => { const sendLogin = useCallback(() => {
const token = getAccessToken(); const token = getAccessToken();
if (!token) { if (!token) {
console.error("Cannot login: no token available"); console.error('Cannot login: no token available');
onErrorRef.current?.("Токен авторизации не найден"); onErrorRef.current?.('Токен авторизации не найден');
return false; return false;
} }
const loginMessage = { const loginMessage = {
event: "user_login", event: 'user_login',
data: { token }, data: { token },
}; };
@ -158,7 +138,7 @@ export const RealtimeProvider = ({
useEffect(() => { useEffect(() => {
if (isConnected && !authAttemptedRef.current) { if (isConnected && !authAttemptedRef.current) {
authAttemptedRef.current = true; authAttemptedRef.current = true;
console.log("WebSocket connected, sending login..."); console.log('WebSocket connected, sending login...');
setTimeout(() => { setTimeout(() => {
sendLogin(); sendLogin();
}, 100); }, 100);
@ -191,7 +171,7 @@ export const RealtimeProvider = ({
const rowId = row.id; const rowId = row.id;
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.("WebSocket не подключен"); onErrorRef.current?.('WebSocket не подключен');
return false; return false;
} }
@ -199,7 +179,7 @@ export const RealtimeProvider = ({
const colId = columnId.slice(5); // убираем префикс "data." const colId = columnId.slice(5); // убираем префикс "data."
const message = { const message = {
event: "cell_edit_start", event: 'cell_edit_start',
data: { data: {
line_id: lineId, line_id: lineId,
column: colId, column: colId,
@ -223,7 +203,7 @@ export const RealtimeProvider = ({
const rowId = row.id; const rowId = row.id;
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.("WebSocket не подключен"); onErrorRef.current?.('WebSocket не подключен');
return false; return false;
} }
@ -231,7 +211,7 @@ export const RealtimeProvider = ({
const colId = columnId.slice(5); const colId = columnId.slice(5);
const message = { const message = {
event: "cell_edit_end", event: 'cell_edit_end',
data: { data: {
line_id: lineId, line_id: lineId,
column: colId, column: colId,
@ -253,14 +233,14 @@ export const RealtimeProvider = ({
const columnId = column.id; const columnId = column.id;
const rowId = row.id; const rowId = row.id;
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.("WebSocket не подключен"); onErrorRef.current?.('WebSocket не подключен');
return false; return false;
} }
const lineId = getRowId(row); const lineId = getRowId(row);
const colId = columnId.slice(5); const colId = columnId.slice(5);
const message = { const message = {
event: "cell_updated", event: 'cell_updated',
data: { data: {
line_id: lineId, line_id: lineId,
line_id_code: row.id, line_id_code: row.id,
@ -282,12 +262,12 @@ export const RealtimeProvider = ({
const addRow = useCallback( const addRow = useCallback(
async (rowData) => { async (rowData) => {
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.("Нет подключения или авторизации"); onErrorRef.current?.('Нет подключения или авторизации');
return false; return false;
} }
const message = { const message = {
event: "row_added", event: 'row_added',
data: rowData, data: rowData,
}; };
addCommonField(message); addCommonField(message);
@ -300,12 +280,12 @@ export const RealtimeProvider = ({
const addProject = useCallback( const addProject = useCallback(
async (rowData) => { async (rowData) => {
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.("Нет подключения или авторизации"); onErrorRef.current?.('Нет подключения или авторизации');
return false; return false;
} }
const message = { const message = {
event: "project_added", event: 'project_added',
data: rowData, data: rowData,
}; };
addCommonField(message); addCommonField(message);
@ -318,12 +298,12 @@ export const RealtimeProvider = ({
const addProgram = useCallback( const addProgram = useCallback(
async (rowData) => { async (rowData) => {
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.("Нет подключения или авторизации"); onErrorRef.current?.('Нет подключения или авторизации');
return false; return false;
} }
const message = { const message = {
event: "program_added", event: 'program_added',
data: rowData, data: rowData,
}; };
addCommonField(message); addCommonField(message);
@ -336,12 +316,12 @@ export const RealtimeProvider = ({
const deleteRow = useCallback( const deleteRow = useCallback(
async (rowId) => { async (rowId) => {
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.("Нет подключения или авторизации"); onErrorRef.current?.('Нет подключения или авторизации');
return false; return false;
} }
const message = { const message = {
event: "row_deleted", event: 'row_deleted',
data: { row_id: rowId }, data: { row_id: rowId },
}; };
addCommonField(message); addCommonField(message);
@ -446,8 +426,7 @@ export const RealtimeProvider = ({
lockedCells, lockedCells,
setLockedCells, setLockedCells,
releaseAllLocks: () => releaseAllLocks(userId), releaseAllLocks: () => releaseAllLocks(userId),
}} }}>
>
{children} {children}
</RealtimeContext.Provider> </RealtimeContext.Provider>
); );

View File

@ -1,20 +1,13 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from "react-toastify"; import { toast } from 'react-toastify';
import { FormsSheetApi } from "../../../api/form_sheet"; import { FormsSheetApi } from '../../../api/form_sheet';
import { ProjectsApi } from "../../../api/projects"; import { ProjectsApi } from '../../../api/projects';
import { useRealtime } from "../contexts/RealtimeContext"; import { useRealtime } from '../contexts/RealtimeContext';
import { import { addRootRow, deleteRow, insertCell, insertProjectRow, updateCells } from '../utils/cellUtils';
addRootRow, import { getParentId, isVspNewRow } from '../utils/rowUtils';
deleteRow,
insertCell,
insertProjectRow,
updateCells,
} from "../utils/cellUtils";
import { getParentId, isVspNewRow } from "../utils/rowUtils";
const useRealtimeData = (formId, sheetName, direction, formType, year) => { const useRealtimeData = (formId, sheetName, direction, formType, year) => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [columnsCurStage, setColumnsCurStage] = useState([]);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -88,7 +81,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
setIsLoading(true); setIsLoading(true);
let res; let res;
if (formType === "PROJECT") { if (formType == 'PROJECT') {
res = await ProjectsApi.getTableData(formId, sheetName, year); res = await ProjectsApi.getTableData(formId, sheetName, year);
} else if (direction !== null) { } else if (direction !== null) {
res = await FormsSheetApi.get(formId, sheetName, { res = await FormsSheetApi.get(formId, sheetName, {
@ -98,14 +91,6 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
res = await FormsSheetApi.get(formId, sheetName); res = await FormsSheetApi.get(formId, sheetName);
} }
const data = res.result.filter((d) => d.depth > -1); const data = res.result.filter((d) => d.depth > -1);
const curStageColumns = res.result.filter((d) => d.depth === -1)[0];
if (curStageColumns) {
const cols = [];
for (const col of curStageColumns.data.editable) {
cols.push(`data.${col.column}`);
}
setColumnsCurStage(cols);
}
if (!data || data.length === 0) { if (!data || data.length === 0) {
setData([]); setData([]);
@ -115,7 +100,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
const hierarchicalData = buildHierarchy(data); const hierarchicalData = buildHierarchy(data);
setData(hierarchicalData); setData(hierarchicalData);
} catch (err) { } catch (err) {
console.error("Failed to load initial data:", err); console.error('Failed to load initial data:', err);
setError(err.message); setError(err.message);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -143,10 +128,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
for (const item of items) { for (const item of items) {
const currentLevel = getTypeLevel(item.row_type); const currentLevel = getTypeLevel(item.row_type);
while ( while (stack.length > 0 && getTypeLevel(stack[stack.length - 1].row_type) >= currentLevel) {
stack.length > 0 &&
getTypeLevel(stack[stack.length - 1].row_type) >= currentLevel
) {
stack.pop(); stack.pop();
} }
@ -164,7 +146,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
} }
// Добавляем текущий узел в стек, если он может иметь детей // Добавляем текущий узел в стек, если он может иметь детей
if (item.row_type !== "INPUT") { if (item.row_type !== 'INPUT') {
stack.push(newNode); stack.push(newNode);
} }
} }
@ -213,12 +195,12 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
[[], null], [[], null],
); );
if (newRow === null) { if (newRow === null) {
toast.error("Ошибка получения данных"); toast.error('Ошибка получения данных');
return; return;
} }
setData((prevData) => { setData((prevData) => {
let data = structuredClone(prevData); let data = structuredClone(prevData);
if (prevData[0]?.row_type !== "ROOT") { if (prevData[0]?.row_type !== 'ROOT') {
const newRowRoot = updates[0]; const newRowRoot = updates[0];
data = addRootRow(data, newRowRoot); data = addRootRow(data, newRowRoot);
} }
@ -233,7 +215,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
const program_id = data.data.program_id; const program_id = data.data.program_id;
const [updatedCells, newRow] = updates.reduce( const [updatedCells, newRow] = updates.reduce(
(res, row) => { (res, row) => {
if (row[0] === "ITEM") { if (row[0] === 'ITEM') {
res[1] = row; res[1] = row;
} else { } else {
res[0].push(row); res[0].push(row);
@ -243,7 +225,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
[[], null], [[], null],
); );
if (newRow === null) { if (newRow === null) {
toast.error("Ошибка получения данных"); toast.error('Ошибка получения данных');
return; return;
} }
setData((prevData) => { setData((prevData) => {
@ -256,7 +238,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
const unsubscribeProgramAdds = subscribeToProgramAdds((msg) => { const unsubscribeProgramAdds = subscribeToProgramAdds((msg) => {
let newRow = msg.result[1][0]; let newRow = msg.result[1][0];
setData((prevData) => { setData((prevData) => {
const hasRoot = prevData.length > 0 && prevData[0].row_type === "ROOT"; const hasRoot = prevData.length > 0 && prevData[0].row_type === 'ROOT';
if (hasRoot) { if (hasRoot) {
newRow = msg.result[1][1]; newRow = msg.result[1][1];
} }
@ -291,7 +273,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
// Обработчик начала редактирования ячейки // Обработчик начала редактирования ячейки
const unsubscribeCellEditStart = subscribeToCellEditStart((data) => { const unsubscribeCellEditStart = subscribeToCellEditStart((data) => {
const dataCell = data.data; const dataCell = data.data;
dataCell.column = "data." + dataCell.column; dataCell.column = 'data.' + dataCell.column;
if (data.is_self) { if (data.is_self) {
setEditingCells(dataCell); setEditingCells(dataCell);
return; return;
@ -311,14 +293,14 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
return; return;
} }
const dataCell = data.data; const dataCell = data.data;
dataCell.column = `data.${dataCell.column}`; dataCell.column = 'data.' + dataCell.column;
const { line_id, column } = dataCell; const { line_id, column } = dataCell;
const cellKey = `${line_id}_${column}`; const cellKey = `${line_id}_${column}`;
setLockedCells((prev) => prev.filter((c) => c !== cellKey)); setLockedCells((prev) => prev.filter((c) => c !== cellKey));
}); });
const unsubscribeErrors = subscribeToErrors((err) => { const unsubscribeErrors = subscribeToErrors((err) => {
console.error("WebSocket error:", err); console.error('WebSocket error:', err);
setError(err); setError(err);
setTimeout(() => setError(null), 5000); setTimeout(() => setError(null), 5000);
}); });
@ -368,7 +350,6 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
return { return {
data, data,
setData, setData,
columnsCurStage,
isConnected, isConnected,
isLoading, isLoading,
error, error,

View File

@ -79,7 +79,7 @@ const EditCellPortal = React.memo(({
}); });
function getColorCell(colors, column, row) { function getColorCell(colors, column, row) {
if (row.original.data.section_code) { if (row.original.data.section_code){
const section_code = row.original.data.section_code[0]; const section_code = row.original.data.section_code[0];
return (sectionCodeColor[section_code] || blueColumn)[row.original?.row_type || row.row_type]; return (sectionCodeColor[section_code] || blueColumn)[row.original?.row_type || row.row_type];
} }
@ -90,7 +90,6 @@ export const getTableColumns = ({
Cell, Cell,
EditCell, EditCell,
columnsConfig, columnsConfig,
columnsCurStage,
onCellUpdateError, onCellUpdateError,
onCellNumberClick, onCellNumberClick,
isCellInvalid, isCellInvalid,
@ -101,13 +100,6 @@ export const getTableColumns = ({
const cellPropsCache = new WeakMap(); const cellPropsCache = new WeakMap();
const editPropsCache = new WeakMap(); const editPropsCache = new WeakMap();
const isEditable = (row, column) => {
if (row.original?.row_type !== 'INPUT') return false;
const keyCol = column.id;
if (columnsCurStage.includes(keyCol)) return true;
return false;
}
const getCachedCellProps = (row, column, table) => { const getCachedCellProps = (row, column, table) => {
const key = `${row.id}_${column.id}`; const key = `${row.id}_${column.id}`;
const value = row.getValue(column.id); const value = row.getValue(column.id);
@ -137,7 +129,7 @@ export const getTableColumns = ({
const props = { const props = {
globalFilter, globalFilter,
columnFilter, columnFilter,
isEditable: isEditable(row, column), isEditable: row.original?.row_type === 'INPUT' || false,
backgroundColor: getColorCell(columnColors, column, row), backgroundColor: getColorCell(columnColors, column, row),
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`], isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
isInvalid, isInvalid,
@ -162,7 +154,7 @@ export const getTableColumns = ({
} }
const props = { const props = {
isEditable: isEditable(row, column), isEditable: row.original?.row_type === 'INPUT' || false,
}; };
rowCache[key] = props; rowCache[key] = props;

View File

@ -1,6 +1,5 @@
// Колонки, которые не показываются при выборе колонок этапа // Колонки, которые не показываются при выборе колонок этапа
export const stageColumnsHiddenFromPicker = [ export const stageColumnsHiddenFromPicker = [
'sort_order',
'data.header.section', 'data.header.section',
'data.header.item_id', 'data.header.item_id',
'data.header.num_group', 'data.header.num_group',