diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index 1478848..7212e7f 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useMemo } from 'react'; import styled from '@emotion/styled'; import { useRealtime } from '../../contexts/RealtimeContext'; +import { toast } from 'react-toastify'; const ContainerForText = styled.span` @@ -42,7 +43,7 @@ const highlightText = (text, searchQuery) => { ); }; -const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color }) => { +const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => { const cellValue = cell.getValue(); const textValue = String(cellValue || ''); @@ -51,14 +52,11 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC return highlightText(textValue, globalFilter); }, [textValue, globalFilter]); - const { - isCellLocked, - getCellLockInfo, - updateCell, - subscribeToErrors, - tryLockCell, - unlockCell - } = useRealtime(); + const lockedStyles = !isEditable ? { + border: '2px solid #d3d3d3', + color: '#525252', + cursor: 'not-allowed', + } : {}; return ( @@ -80,6 +78,7 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC padding: '8px', backgroundColor: backgroundColor, color: color, + ...lockedStyles }} onClick={onClick} > diff --git a/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx b/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx index ec686de..68a87d3 100644 --- a/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx +++ b/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx @@ -7,7 +7,7 @@ const EditCell = ({ refCell, onChange, disabled, value }) => { const left = tdFrag.offsetLeft; const width = refCell.current.offsetParent.offsetWidth; - const translateY = 'translateY(' + trFrag.offsetTop + 'px)'; + const translateY = trFrag.style.transform; function useAutoResize(value) { const ref = useRef(null); @@ -71,7 +71,7 @@ const EditCell = ({ refCell, onChange, disabled, value }) => { }} onChange={handleChangeValue} value={curValue} - onBlur={handleChange} + onBlur={handleChange} /> diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 655c333..39e6f0b 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -1,5 +1,5 @@ // components/RealtimeTable/index.js -import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'; +import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react'; import { createPortal } from 'react-dom'; import { useMaterialReactTable, @@ -25,12 +25,18 @@ import { toast } from 'react-toastify'; const RealtimeTable = ({ formType, formId, sheetName, direction }) => { const { data, setData } = useRealtimeData(formId, sheetName, direction); - const [isLoading, setIsLoading] = useState(true); const [globalFilter, setGlobalFilter] = useState(''); const [showColumnFilters, setShowColumnFilters] = useState(false); const [selectedColumn, setSelectedColumn] = useState(); - const columnVirtualizerInstanceRef = useRef(null); const [rowSelection, setRowSelection] = useState({}); + const [isLoadingData, setIsLoadingData] = useState(false); + const [isPending, startTransition] = useTransition(); + + const handleGlobalFilterChange = (value) => { + startTransition(() => { + setGlobalFilter(value); + }); + }; const { isConnected, @@ -85,7 +91,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { setRowSelection((prev) => ({ [rowId]: !prev[rowId], })); - }, [rowSelection]) + }, []) const columns = useMemo(() => { @@ -108,7 +114,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { } }, [columnsConfig, handleUpdateCell]); - const tableMeta = useMemo(() => ({ updateCell: handleUpdateCell, }), [handleUpdateCell]); @@ -119,13 +124,17 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { ...BASE_TABLE_CONFIG, columns, data: data, - columnVirtualizerInstanceRef, - enableRowVirtualization: false, + enableRowVirtualization: true, + rowVirtualizerOptions: { + overscan: 50, + scrollPaddingStart: 0, + scrollPaddingEnd: 0, + }, enableRowSelection: true, onRowSelectionChange: setRowSelection, onColumnSizingChange: setColumnSizing, onColumnPinningChange: setColumnPinning, - onGlobalFilterChange: setGlobalFilter, + onGlobalFilterChange: handleGlobalFilterChange, state: { columnSizing, columnPinning, @@ -135,7 +144,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { rowSelection }, initialState: { - hiddenColumn: [] + expanded: true }, meta: tableMeta, muiTableHeadCellProps: ({ column }) => ({ @@ -153,22 +162,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { height: '1px', minHeight: '1px', maxHeight: '1px', - opacity: 0, - pointerEvents: 'none', - backgroundColor: 'transparent', - '& th': { - height: '1px', - minHeight: '1px', - maxHeight: '1px', - }, + visibility: 'hidden', }, }, muiTablePaperProps: getTablePaperStyles(), muiTableContainerProps: { ref: containerRef, sx: { - overflowX: 'auto', - overflowY: 'auto', position: 'relative', contain: 'layout', minHeight: '100%', @@ -198,6 +198,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { const table = useMaterialReactTable(tableConfig); + useEffect(() => { + if (table && data && columns.length !== 0) { + setIsLoadingData(true) + } + }, [data, columns, table]) + const handleAddRow = useCallback(() => { const allRows = table.getRowModel().rows; @@ -222,7 +228,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { const row = selectedRows[0]; const expense_item_id = row.original.data.line_id; contextDeleteRow(expense_item_id) + setRowSelection({}); }, [rowSelection, table]) + if (!data) { return
Загрузка...
; } @@ -237,11 +245,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { onToggleColumnVisibility={handleToggleColumnVisibility} columnVisibility={columnVisibility} onChangeSizeMult={setSizeMult} - onGlobalFilterChange={setGlobalFilter} + onGlobalFilterChange={handleGlobalFilterChange} sizeMult={sizeMult} onChangeShowColumnFilters={setShowColumnFilters} onAddRow={handleAddRow} onDeleteRow={handleDeleteRow} + isLoadingData={isLoadingData} />
@@ -265,6 +274,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { onHeaderSelect={setSelectedColumn} selectHeader={selectedColumn} onChangeWidth={handleSetColumnWidth} + isLoadingData={isLoadingData} />, headerPortalRef.current, )} diff --git a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx index 494f694..6477c84 100644 --- a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx +++ b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx @@ -31,6 +31,7 @@ const SettingPanel = ({ onChangeShowColumnFilters, onAddRow, onDeleteRow, + isLoadingData, }) => { const [isPinned, setIsPinned] = useState(false); const [columnTree, setColumnTree] = useState(); @@ -41,7 +42,7 @@ const SettingPanel = ({ if (!table) return; setColumnTree(table.getAllColumns()); setShowColumnFilters(table.getState().showColumnFilters); - }, [table]); + }, [table, isLoadingData]); useEffect(() => { if (!selectedColumn) return; diff --git a/web/src/components/RealtimeTable/TableHead/TableHead.jsx b/web/src/components/RealtimeTable/TableHead/TableHead.jsx index fe15262..45ba813 100644 --- a/web/src/components/RealtimeTable/TableHead/TableHead.jsx +++ b/web/src/components/RealtimeTable/TableHead/TableHead.jsx @@ -131,7 +131,6 @@ const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => { // Если колонка не закреплена, текст должен начинаться после всех закрепленных колонок const getTextLeftOffset = () => { if (isPinned) { - // Для закрепленных колонок текст остается на месте return '0'; } // Для незакрепленных колонок текст должен начинаться после всех закрепленных @@ -215,7 +214,7 @@ const FilterRow = ({ table }) => { style={{ display: 'flex', position: 'sticky', - top: '1.5rem', // После строки с номерами колонок + top: '1.5rem', zIndex: 20, backgroundColor: 'white', }} @@ -338,11 +337,16 @@ export const TableHead = ({ onHeaderSelect, selectHeader, onChangeWidth, + isLoadingData, }) => { const handleHeaderClick = (header) => { onHeaderSelect(header); }; + if (!isLoadingData) { + return
+ } + return ( <>
{ return; } - const maxDepth = data.reduce( - (prev, current) => { - return (prev.depth || 0) > (current.depth || 0) ? prev : current; - }, - { depth: 0 }, - ).depth; - - const formattedData = formatedToSubRows(maxDepth, data); - setData(formattedData); + const hierarchicalData = buildHierarchy(data); + setData(hierarchicalData); } catch (err) { console.error("Failed to load initial data:", err); setError(err.message); @@ -104,7 +97,54 @@ const useRealtimeData = (formId, sheetName, direction) => { }; loadInitialData(); - }, [formId, sheetName, formatedToSubRows]); + }, [formId, sheetName]); + + const buildHierarchy = (items) => { + const result = []; + const stack = []; + + const getTypeLevel = (type) => { + const levels = { + ROOT: 0, + GROUP: 1, + ITEM: 2, + SUB_ITEM: 3, + INPUT: 4, + }; + return levels[type] ?? 999; + }; + + for (const item of items) { + const currentLevel = getTypeLevel(item.row_type); + + while ( + stack.length > 0 && + getTypeLevel(stack[stack.length - 1].row_type) >= currentLevel + ) { + stack.pop(); + } + + const parent = stack.length > 0 ? stack[stack.length - 1] : null; + + const newNode = { ...item, subRows: [] }; + + if (parent) { + if (!parent.subRows) { + parent.subRows = []; + } + parent.subRows.push(newNode); + } else { + result.push(newNode); + } + + // Добавляем текущий узел в стек, если он может иметь детей + if (item.row_type !== "INPUT") { + stack.push(newNode); + } + } + + return result; + }; useEffect(() => { setIsConnected(wsConnected); diff --git a/web/src/components/RealtimeTable/tableColumns.jsx b/web/src/components/RealtimeTable/tableColumns.jsx index d6fc397..8c9af2a 100644 --- a/web/src/components/RealtimeTable/tableColumns.jsx +++ b/web/src/components/RealtimeTable/tableColumns.jsx @@ -9,7 +9,8 @@ function EditCellPortal({ EditCell, onSaveStart, onSaveEnd, - onError + onError, + isEditable }) { const ref = useRef(null); const [refTbody, setRefTbody] = useState(false); @@ -25,17 +26,7 @@ function EditCellPortal({ const handleChange = async (val) => { table.setEditingCell(null); - if (isSaving) return; - - setIsSaving(true); - onSaveStart?.(); - - // Сохраняем старое значение на случай ошибки - const oldValue = cell.getValue(); - try { - - // Используем updateCell из контекста (передается через meta) if (table.options.meta?.updateCell) { const success = await table.options.meta.updateCell( cell.row, @@ -44,32 +35,15 @@ function EditCellPortal({ ); if (success) { console.log('Cell updated successfully via WebSocket'); - onSaveEnd?.(true); - table.setEditingCell(null); } else { console.error('Failed to update cell via WebSocket'); const errorMsg = 'Не удалось сохранить изменение'; onError?.(errorMsg); - onSaveEnd?.(false); - // Возвращаем предыдущее значение - } - } else { - // Если updateCell нет в meta, используем onCellUpdate из пропсов (fallback) - console.warn('WebSocket updateCell not configured in meta'); - if (table.options.meta?.onCellUpdate) { - table.options.meta.onCellUpdate(cell.row.id, cell.column.id, val); - } - onSaveEnd?.(true); - table.setEditingCell(null); - } + } } catch (error) { console.error('Error updating cell:', error); onError?.(error.message); - onSaveEnd?.(false); - - } finally { - setIsSaving(false); } }; @@ -88,7 +62,7 @@ function EditCellPortal({ refCell={ref} cell={cell} value={cell.getValue()} - disabled={isDisabled || isSaving} + disabled={!isEditable} onChange={handleChange} />, refTbody, @@ -131,8 +105,9 @@ export const getTableColumns = ({ column={column} cell={cell} globalFilter={ - table.getState().globalFilter || column.getFilterValue() + table.getState().globalFilter } + isEditable={row.original?.row_type === 'INPUT' || false} backgroundColor={columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type]} color={columnColors[column.id]?.color_type?.['COLOR']} isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]} @@ -144,20 +119,8 @@ export const getTableColumns = ({ cell={cell} table={table} EditCell={EditCell} - onSaveStart={() => { - if (table.options.meta?.setUpdatingCell) { - table.options.meta.setUpdatingCell(row.id, column.id, true); - } - }} - onSaveEnd={(success) => { - if (table.options.meta?.setUpdatingCell) { - table.options.meta.setUpdatingCell(row.id, column.id, false); - } - if (!success && onCellUpdateError) { - onCellUpdateError(row.id, column.id, 'Failed to save'); - } - }} onError={onCellUpdateError} + isEditable={row.original?.row_type === 'INPUT' || false} /> ); @@ -177,10 +140,9 @@ export const getTableColumns = ({ enableEditing: false, enablePinning: true, Cell: ({ cell, row }) => { - return {console.log('TEST', row.id); onCellNumberClick(row.id)}} - >{cell.getValue()}; + return }, }; diff --git a/web/src/components/RealtimeTable/utils/cellUtils.js b/web/src/components/RealtimeTable/utils/cellUtils.js index 4753e9d..509286a 100644 --- a/web/src/components/RealtimeTable/utils/cellUtils.js +++ b/web/src/components/RealtimeTable/utils/cellUtils.js @@ -1,5 +1,4 @@ export function setNewValueToData(data, colId, rowId, value) { - // 1. Находим целевой объект по rowId(индексы вложенных subRows) let targetObject = data; if (rowId && rowId.length) { const indices = rowId.split(".").map(Number); diff --git a/web/src/components/common/TableCard/TableCard.jsx b/web/src/components/common/TableCard/TableCard.jsx index fa12a9d..d5e4223 100644 --- a/web/src/components/common/TableCard/TableCard.jsx +++ b/web/src/components/common/TableCard/TableCard.jsx @@ -14,7 +14,7 @@ const TableCard = ({ table, onNavigate }) => { }; return ( -
+
diff --git a/web/src/components/common/TableList/TableList.jsx b/web/src/components/common/TableList/TableList.jsx index 28be971..e0afa57 100644 --- a/web/src/components/common/TableList/TableList.jsx +++ b/web/src/components/common/TableList/TableList.jsx @@ -26,7 +26,7 @@ const TablesList = ({ return ( handleNavigate(path)} table={table} />