diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index 6798e72..20cdc41 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -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: @@ -217,4 +227,4 @@ const Cell = React.memo(CellComponent, (prevProps, nextProps) => { ); }); -export default Cell; +export default Cell; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 58b35c5..920302d 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -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 ( <> { 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} />
diff --git a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx index c662673..2c6bf29 100644 --- a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx +++ b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx @@ -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 = ({ {/* проверяем на 4 форму */} - {formType == FORM_TYPE_OPTIONS[3] && ( + {formType === FORM_TYPE_OPTIONS[3] && ( <> @@ -142,6 +161,11 @@ const SettingPanel = ({ )} + + + + + )} @@ -152,6 +176,16 @@ const SettingPanel = ({ ) : ( '' )} + + + + + @@ -181,4 +215,4 @@ const SettingPanel = ({ ); }; -export default SettingPanel; +export default SettingPanel; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/constants/columnColors.js b/web/src/components/RealtimeTable/constants/columnColors.js index 054d7c9..817be52 100644 --- a/web/src/components/RealtimeTable/constants/columnColors.js +++ b/web/src/components/RealtimeTable/constants/columnColors.js @@ -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", }; diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js index ae7b42f..683fe9f 100644 --- a/web/src/components/RealtimeTable/contexts/RealtimeContext.js +++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js @@ -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} ); diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js index 49339de..303af97 100644 --- a/web/src/components/RealtimeTable/hooks/useRealtimeData.js +++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js @@ -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, diff --git a/web/src/components/RealtimeTable/tableColumns.jsx b/web/src/components/RealtimeTable/tableColumns.jsx index 2e578aa..3fb6e8c 100644 --- a/web/src/components/RealtimeTable/tableColumns.jsx +++ b/web/src/components/RealtimeTable/tableColumns.jsx @@ -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; diff --git a/web/src/components/Stages/constants.js b/web/src/components/Stages/constants.js index a32dba4..9e6d491 100644 --- a/web/src/components/Stages/constants.js +++ b/web/src/components/Stages/constants.js @@ -1,5 +1,6 @@ // Колонки, которые не показываются при выборе колонок этапа export const stageColumnsHiddenFromPicker = [ + 'sort_order', 'data.header.section', 'data.header.item_id', 'data.header.num_group',