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',
};
const EDITABLE_STYLES = {
cursor: 'pointer',
border: '2px solid #d8e4ea94',
};
const NOT_EDITABLE_STYLES = {
cursor: 'default',
};
const LOCK_BADGE_STYLES = {
position: 'absolute',
top: '2px',
@ -177,8 +186,9 @@ const CellComponent = ({
color: textColor,
...(isLocked ? LOCKED_STYLES : {}),
...(isInvalid ? INVALID_STYLES : {}),
...(isEditable ? EDITABLE_STYLES : NOT_EDITABLE_STYLES),
}),
[backgroundColor, isInvalid, isLocked, textColor],
[backgroundColor, isInvalid, isLocked, textColor, isEditable],
);
const lockBadge = useMemo(() => {
@ -187,10 +197,10 @@ const CellComponent = ({
}, [isLocked]);
const handleClick = useCallback(() => {
if (!isLocked && onClick) {
if (!isLocked && isEditable && onClick) {
onClick();
}
}, [isLocked, onClick]);
}, [isLocked, isEditable, onClick]);
return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>

View File

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

View File

@ -1,15 +1,17 @@
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 { exportSheet, exportSheetProject } from '../../../utils/exportFile';
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
import CollapsibleTree from '../../common/CollapsibleTree';
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 { Panel } from './SettingPanel.style';
import ValidationErrorsAlert from './ValidationErrorsAlert';
import ZoomSlider from './ZoomSlider';
import { collectLeafColumns } from '../utils/columnUtils';
import { stageColumnsHiddenFromPicker } from '../../Stages/constants';
const SettingPanel = ({
selectedColumnId,
@ -20,6 +22,7 @@ const SettingPanel = ({
onUnpinColumn,
onToggleColumnVisibility,
columnVisibility,
columnsCurStage,
onChangeSizeMult,
onGlobalFilterChange,
sizeMult,
@ -37,6 +40,8 @@ const SettingPanel = ({
formType,
validationErrors = [],
onNavigateToColumn,
showOnlyDepth2,
onToggleDepthVisibility,
}) => {
const [isPinned, setIsPinned] = useState(false);
const [isExporting, setIsExporting] = useState(false);
@ -81,6 +86,20 @@ const SettingPanel = ({
await exportSheet(formId, sheetName, direction);
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 (
<>
@ -125,7 +144,7 @@ const SettingPanel = ({
</IconButton>
</Tooltip>
{/* проверяем на 4 форму */}
{formType == FORM_TYPE_OPTIONS[3] && (
{formType === FORM_TYPE_OPTIONS[3] && (
<>
<Tooltip title='Добавить программу'>
<IconButton onClick={onAddProgram} variant='outlined'>
@ -142,6 +161,11 @@ const SettingPanel = ({
</Tooltip>
</>
)}
<Tooltip title='Скрыть статьи расходов'>
<IconButton onClick={onToggleDepthVisibility} variant='outlined'>
<Eye />
</IconButton>
</Tooltip>
</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>
<Divider orientation='vertical' flexItem />
<ZoomSlider onChange={onChangeSizeMult} curScale={sizeMult} />

View File

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

View File

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

View File

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

View File

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

View File

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