Merge pull request 'add column of stage for table' (#79) from stage-fron-table into test

Reviewed-on: #79
This commit is contained in:
PotapovaA 2026-08-10 18:43:47 +03:00
commit 8c1341bba8
8 changed files with 262 additions and 130 deletions

View File

@ -31,6 +31,15 @@ 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',
@ -177,8 +186,9 @@ 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], [backgroundColor, isInvalid, isLocked, textColor, isEditable],
); );
const lockBadge = useMemo(() => { const lockBadge = useMemo(() => {
@ -187,10 +197,10 @@ const CellComponent = ({
}, [isLocked]); }, [isLocked]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (!isLocked && onClick) { if (!isLocked && isEditable && onClick) {
onClick(); onClick();
} }
}, [isLocked, onClick]); }, [isLocked, isEditable, onClick]);
return ( return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> // biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>

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, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); const { data, setData, columnsCurStage, 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,6 +48,9 @@ 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);
@ -174,12 +177,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
}, []); }, []);
const columns = useMemo(() => { const columns = useMemo(() => {
if (!columnsConfig || !columnsConfig.columns) return []; if (!columnsConfig || !columnsConfig.columns || columnsCurStage.length === 0) 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,
@ -191,6 +195,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
} }
}, [ }, [
columnsConfig, columnsConfig,
columnsCurStage,
handleCellUpdateError, handleCellUpdateError,
handleClickRowCell, handleClickRowCell,
handleUpdateCell, handleUpdateCell,
@ -222,6 +227,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
columnSizing, columnSizing,
columnPinning, columnPinning,
columnVisibility, columnVisibility,
expanded,
globalFilter, globalFilter,
showColumnFilters, showColumnFilters,
rowSelection, rowSelection,
@ -229,6 +235,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
tableMeta, tableMeta,
setColumnSizing, setColumnSizing,
setColumnPinning, setColumnPinning,
setExpanded,
editingCell, editingCell,
contextStartEditing, contextStartEditing,
}) => ({ }) => ({
@ -263,6 +270,7 @@ 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,
@ -272,6 +280,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
showColumnFilters, showColumnFilters,
rowSelection, rowSelection,
editingCell, editingCell,
expanded
}, },
initialState: { initialState: {
expanded: true, expanded: true,
@ -312,13 +321,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,
@ -326,6 +335,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
tableMeta, tableMeta,
setColumnSizing, setColumnSizing,
setColumnPinning, setColumnPinning,
setExpanded,
editingCell, editingCell,
contextStartEditing, contextStartEditing,
}), [ }), [
@ -342,6 +352,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setColumnSizing, setColumnSizing,
setColumnPinning, setColumnPinning,
editingCell, editingCell,
expanded,
]); ]);
const table = useMaterialReactTable(tableConfig); const table = useMaterialReactTable(tableConfig);
@ -380,7 +391,6 @@ 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);
@ -481,12 +491,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
return handleOpenModalSelectExpenseItem(); return handleOpenModalSelectExpenseItem();
} }
return handleAddRow(); return handleAddRow();
}, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow]); }, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]);
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);
@ -516,6 +526,32 @@ 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
@ -527,6 +563,7 @@ 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}
@ -540,10 +577,12 @@ 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,15 +1,17 @@
import { Divider, IconButton, Stack, Tooltip } from '@mui/material'; import { Divider, IconButton, Stack, Tooltip } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } 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, Minus, Pin, Plus, Program, Project, RemoveLine, Search } from '../../common/icons/icons'; import { AddLine, Eye, 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,
@ -20,6 +22,7 @@ const SettingPanel = ({
onUnpinColumn, onUnpinColumn,
onToggleColumnVisibility, onToggleColumnVisibility,
columnVisibility, columnVisibility,
columnsCurStage,
onChangeSizeMult, onChangeSizeMult,
onGlobalFilterChange, onGlobalFilterChange,
sizeMult, sizeMult,
@ -37,6 +40,8 @@ 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);
@ -81,6 +86,20 @@ 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 (
<> <>
@ -125,7 +144,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'>
@ -142,6 +161,11 @@ const SettingPanel = ({
</Tooltip> </Tooltip>
</> </>
)} )}
<Tooltip title='Скрыть статьи расходов'>
<IconButton onClick={onToggleDepthVisibility} variant='outlined'>
<Eye />
</IconButton>
</Tooltip>
</GroupByObject> </GroupByObject>
</> </>
)} )}
@ -152,6 +176,16 @@ 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} />

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: '#FFFFFF', SUB_ITEM: "#EAF0DD",
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: '#FFFFFF', SUB_ITEM: "#FAEBDC",
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: '#FFFFFF', SUB_ITEM: "#E5B7B6",
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: '#FFFFFF', SUB_ITEM: "#C5D8F0",
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: '#FFFFFF', SUB_ITEM: "rgb(211, 228, 248)",
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: '#FFFFFF', SUB_ITEM: "#FFFFCC",
INPUT: '#FFFFFF', INPUT: "#FFFFFF",
COLOR: '#000000', COLOR: "#000000",
}; };

View File

@ -1,28 +1,45 @@
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; import {
import { toast } from 'react-toastify'; createContext,
import { useWebSocket } from '../hooks/useWebSocket'; useCallback,
import { getRowId } from '../utils/rowUtils'; useContext,
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 = ({ children, formId, sheetName, direction, year, userId, isProject = false }) => { export const RealtimeProvider = ({
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_ROOT_PATH || 'ws://localhost:8000').replace( process.env.REACT_APP_API_URL ||
/^http/, process.env.REACT_APP_API_URL ||
'ws', process.env.REACT_APP_ROOT_PATH ||
)}` + "ws://localhost:8000"
(!isProject ).replace(/^http/, "ws")}${
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}` !isProject
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`); ? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`
}`;
//для ячеек которые редактируются другими пользователями //для ячеек которые редактируются другими пользователями
const [lockedCells, setLockedCells] = useState([]); const [lockedCells, setLockedCells] = useState([]);
@ -30,65 +47,68 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
// Обработка ошибок // Обработка ошибок
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) ? data.result : [data.result]; const updatedCells = Array.isArray(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 } = useWebSocket(wsUrl, handleMessage); const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } =
useWebSocket(wsUrl, handleMessage);
const onCellUpdateRef = useRef(null); const onCellUpdateRef = useRef(null);
const onRowAddRef = useRef(null); const onRowAddRef = useRef(null);
@ -105,16 +125,16 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
// Функция для получения токена из 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;
} }
}, []); }, []);
@ -122,13 +142,13 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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 },
}; };
@ -138,7 +158,7 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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);
@ -171,7 +191,7 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
const rowId = row.id; const rowId = row.id;
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.('WebSocket не подключен'); onErrorRef.current?.("WebSocket не подключен");
return false; return false;
} }
@ -179,7 +199,7 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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,
@ -203,7 +223,7 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
const rowId = row.id; const rowId = row.id;
if (!isConnectedRef) { if (!isConnectedRef) {
onErrorRef.current?.('WebSocket не подключен'); onErrorRef.current?.("WebSocket не подключен");
return false; return false;
} }
@ -211,7 +231,7 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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,
@ -233,14 +253,14 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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,
@ -262,12 +282,12 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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);
@ -280,12 +300,12 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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);
@ -298,12 +318,12 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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);
@ -316,12 +336,12 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
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);
@ -426,7 +446,8 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
lockedCells, lockedCells,
setLockedCells, setLockedCells,
releaseAllLocks: () => releaseAllLocks(userId), releaseAllLocks: () => releaseAllLocks(userId),
}}> }}
>
{children} {children}
</RealtimeContext.Provider> </RealtimeContext.Provider>
); );

View File

@ -1,13 +1,20 @@
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 { addRootRow, deleteRow, insertCell, insertProjectRow, updateCells } from '../utils/cellUtils'; import {
import { getParentId, isVspNewRow } from '../utils/rowUtils'; addRootRow,
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);
@ -81,7 +88,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, {
@ -91,6 +98,14 @@ 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([]);
@ -100,7 +115,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);
@ -128,7 +143,10 @@ 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 (stack.length > 0 && getTypeLevel(stack[stack.length - 1].row_type) >= currentLevel) { while (
stack.length > 0 &&
getTypeLevel(stack[stack.length - 1].row_type) >= currentLevel
) {
stack.pop(); stack.pop();
} }
@ -146,7 +164,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
} }
// Добавляем текущий узел в стек, если он может иметь детей // Добавляем текущий узел в стек, если он может иметь детей
if (item.row_type !== 'INPUT') { if (item.row_type !== "INPUT") {
stack.push(newNode); stack.push(newNode);
} }
} }
@ -195,12 +213,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);
} }
@ -215,7 +233,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);
@ -225,7 +243,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) => {
@ -238,7 +256,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];
} }
@ -273,7 +291,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;
@ -293,14 +311,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);
}); });
@ -350,6 +368,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
return { return {
data, data,
setData, setData,
columnsCurStage,
isConnected, isConnected,
isLoading, isLoading,
error, error,

View File

@ -90,6 +90,7 @@ export const getTableColumns = ({
Cell, Cell,
EditCell, EditCell,
columnsConfig, columnsConfig,
columnsCurStage,
onCellUpdateError, onCellUpdateError,
onCellNumberClick, onCellNumberClick,
isCellInvalid, isCellInvalid,
@ -100,6 +101,13 @@ 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);
@ -129,7 +137,7 @@ export const getTableColumns = ({
const props = { const props = {
globalFilter, globalFilter,
columnFilter, columnFilter,
isEditable: row.original?.row_type === 'INPUT' || false, isEditable: isEditable(row, column),
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,
@ -154,7 +162,7 @@ export const getTableColumns = ({
} }
const props = { const props = {
isEditable: row.original?.row_type === 'INPUT' || false, isEditable: isEditable(row, column),
}; };
rowCache[key] = props; rowCache[key] = props;

View File

@ -1,5 +1,6 @@
// Колонки, которые не показываются при выборе колонок этапа // Колонки, которые не показываются при выборе колонок этапа
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',