import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react'; import { createPortal } from 'react-dom'; import { useMaterialReactTable, MaterialReactTable, } from 'material-react-table'; import useRealtimeData from './hooks/useRealtimeData'; import { getTableColumns } from './tableColumns'; import { Cell, EditCell } from './index'; import { TableHead } from './TableHead/TableHead'; import SettingsPanel from './SettingPanel/SettingPanel'; import { ColumnSelectionOverlay } from './ColumnSelectionOverlay/ColumnSelectionOverlay'; import { useColumnSettings } from './hooks/useColumnSettings'; import { useTableScale } from './hooks/useTableScale'; import { useHeaderPortal } from './hooks/useHeaderPortal'; import { BASE_TABLE_CONFIG, getTableBodyCellProps, getTablePaperStyles, TABLE_ROW_HEIGHT, } from './constants/tableConfig'; import { useRealtime } from './contexts/RealtimeContext'; import { toast } from 'react-toastify'; import { CircularProgress } from '@mui/material'; import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig'; import { SelectVspModal } from './Modals/SelectVspModal'; import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal'; import { getRowId } from './utils/rowUtils'; import { FORM_TYPE_OPTIONS } from '../../constants/constants'; 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 [globalFilter, setGlobalFilter] = useState(''); const [showColumnFilters, setShowColumnFilters] = useState(false); const [selectedColumnId, setSelectedColumnId] = useState(); const [rowSelection, setRowSelection] = useState({}); const [isLoadingData, setIsLoadingData] = useState(false); const [isPending, startTransition] = useTransition(); const [editingCell, setEditingCell] = useState(null); const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false); const [isOpenModalCreateProgram, setIsOpenModalCreateProgram] = useState(false); const [isOpenModalCreateProject, setIsOpenModalCreateProject] = useState(false); const [isProgramCreate, setIsProgramCreate] = useState(false) const isVspAdditionRow = useMemo(() => { if (!formType || !sheetName) return false; if (!additionVspRowTable[formType]) return false; return additionVspRowTable[formType].includes(sheetName); }, [formType, sheetName]); const isAdditionExpenseItem = useMemo(() => { if (!formType || !sheetName) return false; if (!additionExpenseRowTable[formType]) return false; return additionExpenseRowTable[formType].includes(sheetName); }, [formType, sheetName]); const handleGlobalFilterChange = useCallback( debounce((value) => { startTransition(() => { setGlobalFilter(value); }); }, 300), [] ); const { isConnected, isAuthenticated, subscribeToErrors, subscribeToAuthSuccess, error: wsError, updateCell: contextUpdateCell, addRow: contextAddRow, startEditing: contextStartEditing, addProgram: contextAddProgram, addProject: contextAddProject, } = useRealtime(); const { columnSizing, columnPinning, columnVisibility, setColumnSizing, setColumnPinning, handlePinColumn, handleUnpinColumn, handleSetColumnWidth, handleToggleColumnVisibility, } = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`); const { headerPortalRef, containerRef } = useHeaderPortal(); const rowVirtualizerRef = useRef(null); const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale(); useEffect(() => { rowVirtualizerRef.current?.measure?.(); }, [sizeMult]); const [columnsConfig, setColumnsConfig] = useState(null); const configCache = useRef(new Map()); useEffect(() => { if (!formType || !sheetName) return; const cacheKey = `${formType}_${sheetName}`; if (configCache.current.has(cacheKey)) { setColumnsConfig(configCache.current.get(cacheKey)); return; } const loadConfig = async () => { try { const { config } = await import(`./constants/${formType}/${sheetName}.js`); configCache.current.set(cacheKey, config.config); setColumnsConfig(config.config); } catch (error) { console.error(`Failed to load config for ${formType}/${sheetName}:`, error); setColumnsConfig({ columns: [], colors: {} }); } }; loadConfig(); }, [formType, sheetName]); const handleUpdateCell = useCallback(async (row, column, value) => { return await contextUpdateCell(row, column, value); }, [contextUpdateCell]); const handleClickRowCell = useCallback((rowId) => { setRowSelection((prev) => ({ [rowId]: !prev[rowId], })); }, []) const handleColumnSelect = useCallback((columnId) => { setSelectedColumnId((prev) => { const next = prev === columnId ? undefined : columnId; return next; }); }, []); useEffect(() => { if (!selectedColumnId) return; const handleDocumentClick = (e) => { if (e.target.closest('[data-col-select-id]')) return; if (e.target.closest('[data-pin-panel]')) return; setSelectedColumnId(undefined); }; document.addEventListener('mousedown', handleDocumentClick); return () => document.removeEventListener('mousedown', handleDocumentClick); }, [selectedColumnId]); const handleCellUpdateError = useCallback((rowId, columnId, error) => { console.error(`Error updating cell ${rowId}_${columnId}:`, error); toast.error('Ошибка обновления ячейки'); }, []); const columns = useMemo(() => { if (!columnsConfig || !columnsConfig.columns) return []; try { return getTableColumns({ Cell, EditCell, columnsConfig, onCellUpdate: handleUpdateCell, onCellNumberClick: handleClickRowCell, onCellUpdateError: handleCellUpdateError, }); } catch (error) { console.error('Error creating columns:', error); return []; } }, [columnsConfig]); const selectedColumnIdRef = useRef(selectedColumnId); useEffect(() => { selectedColumnIdRef.current = selectedColumnId; }, [selectedColumnId]); const tableMeta = useMemo(() => ({ updateCell: handleUpdateCell, getSelectedColumnId: () => selectedColumnIdRef.current, }), [handleUpdateCell]); const ROW_VIRTUALIZER_OPTIONS = { overscan: 5, scrollPaddingStart: 0, scrollPaddingEnd: 0, estimateSize: () => TABLE_ROW_HEIGHT, measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT, }; const createTableConfig = ({ columns, data, columnSizing, columnPinning, columnVisibility, globalFilter, showColumnFilters, rowSelection, containerRef, tableMeta, setColumnSizing, setColumnPinning, editingCell, contextStartEditing, }) => ({ ...BASE_TABLE_CONFIG, columns, data, enableRowVirtualization: true, enableColumnVirtualization: true, rowVirtualizerInstanceRef: rowVirtualizerRef, rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS, onEditingCellChange: (cell) => { if (cell && !editingCell) { contextStartEditing?.(cell.row, cell.column); } }, columnVirtualizerOptions: ({ table }) => ({ overscan: 10, measureElement: (el) => { if (!el) return 150; const index = Number(el?.getAttribute?.('data-index')); const isPinned = Boolean(el?.getAttribute?.('data-pinned')); if (isPinned) { const colId = table.getState().columnPinning.left[index]; const column = table.getColumn(colId); return column?.getSize() ?? 150; } const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()] return allCols[index]?.getSize() ?? 150; }, }), enableRowSelection: true, onRowSelectionChange: setRowSelection, onColumnSizingChange: setColumnSizing, onColumnPinningChange: setColumnPinning, onGlobalFilterChange: handleGlobalFilterChange, state: { columnSizing, columnPinning, columnVisibility, globalFilter, showColumnFilters, rowSelection, editingCell, }, initialState: { expanded: true, }, meta: tableMeta, muiTableHeadCellProps: { sx: { boxSizing: 'border-box' }, }, muiTableBodyCellProps: getTableBodyCellProps, muiTableHeadProps: { sx: { display: 'table-header-group', height: '1px', minHeight: '1px', maxHeight: '1px', visibility: 'hidden', '& svg': { height: '1px', minHeight: '1px', maxHeight: '1px', visibility: 'hidden', } }, }, muiTablePaperProps: getTablePaperStyles(), muiTableContainerProps: { ref: containerRef, sx: { position: 'relative', contain: 'layout', minHeight: '100%', '& .MuiTable-root': { position: 'relative' }, }, }, muiTableHeadCellFilterTextFieldProps: { placeholder: 'Поиск...', size: 'small', }, }); // Конфигурация таблицы const tableConfig = useMemo(() => createTableConfig({ columns, data, columnSizing, columnPinning, columnVisibility, globalFilter, showColumnFilters, rowSelection, containerRef, tableMeta, setColumnSizing, setColumnPinning, editingCell, contextStartEditing, }), [ columns, data, columnSizing, columnPinning, columnVisibility, globalFilter, showColumnFilters, rowSelection, containerRef, tableMeta, setColumnSizing, setColumnPinning, editingCell, ]); const table = useMaterialReactTable(tableConfig); useEffect(() => { if (!dataEditingCells?.line_id) { setEditingCell(null); return; } if (editingCell) return; // Отложить поиск до следующего тика requestAnimationFrame(() => { try { const row = table.getRow(dataEditingCells.line_id); const cell = row?.getVisibleCells().find( c => c.column.id === dataEditingCells.column ); if (cell) setEditingCell(cell); } catch (error) { console.error(error); } }); }, [dataEditingCells, editingCell, table]); const [isFirstLoad, setIsFirstLoad] = useState(true); useEffect(() => { if (isFirstLoad && table && data && columns.length !== 0) { setIsFirstLoad(false); setIsLoadingData(true); } }, [data, columns, table, isFirstLoad]); const handleAddRow = useCallback(() => { const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); if (selectedRows.length === 0) { toast.error('Выделите строку для вставки'); return; } const row = selectedRows[0]; const expense_item_id = row.original.data.header.expense_item_id; contextAddRow({ expense_item_id: expense_item_id }) }, [rowSelection, table]) const handleAddVspRow = useCallback((vsp_id) => { contextAddRow({ vsp_id: vsp_id }); }, []); const handleAddExpenseItemRow = useCallback((expense_item) => { const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); const row = selectedRows[0]; contextAddRow({ expense_item_id: expense_item.id, project_id: row.original.data.header.project_id }); }, [rowSelection]); const handleAddProgramRow = useCallback((name) => { contextAddProgram({ name: name }); }, []); const handleAddProjectRow = useCallback((name) => { const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); const row = selectedRows[0]; contextAddProject({ name: name, program_id: row.original.data.header.program_id }); }, [rowSelection]); const handleOpenModalSelectVsp = useCallback(() => { setIsOpenModalSelectVsp(true); }, []) const handleOpenModalSelectExpenseItem = useCallback(() => { const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); if (selectedRows.length === 0) { toast.error('Выделите строку для вставки'); return; } const row = selectedRows[0]; if (row.original.row_type !== 'ITEM') { toast.error('Выделите строку проекта'); return; } setIsOpenModalSelectExpenseItem(true); }, [rowSelection]) const handleDeleteRow = useCallback(() => { const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); if (selectedRows.length === 0) { toast.error('Выделите строку для удаления'); return; } const row = selectedRows[0]; const rowId = getRowId(row); contextDeleteRow(rowId); setRowSelection({}); }, [rowSelection, table]) const addRow = useCallback(() => { if (isVspAdditionRow) { return handleOpenModalSelectVsp(); } if (isAdditionExpenseItem) { return handleOpenModalSelectExpenseItem(); } return handleAddRow(); }, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow]); const addProgram = useCallback(() => { setIsProgramCreate(true); setIsOpenModalCreateProgram(true); }, [setIsOpenModalCreateProgram]) const addProject = useCallback(() => { setIsProgramCreate(false); const allRows = table.getRowModel().rows; const selectedRows = allRows.filter(row => rowSelection[row.id]); if (selectedRows.length === 0) { toast.error('Выделите строку для вставки'); return; } const row = selectedRows[0]; if (row.original.row_type !== 'GROUP') { toast.error('Выделите строку программы'); return; } setIsOpenModalCreateProgram(true); }, [setIsOpenModalCreateProgram, rowSelection]) useEffect(() => { return () => { setData([]); setRowSelection({}); setEditingCell(null); if (rowVirtualizerRef.current) { rowVirtualizerRef.current = null; } }; }, []); return ( <>
{isTableLoading && (
)} {headerPortalRef.current && createPortal( theme.zIndex.modal - 1 }} selectedColumnId={selectedColumnId} onColumnSelect={handleColumnSelect} onChangeWidth={handleSetColumnWidth} isLoadingData={isLoadingData} />, headerPortalRef.current, )}
setIsOpenModalSelectVsp(false)} formId={formId} onSelect={handleAddVspRow} title="Выбор ВСП" /> setIsOpenModalSelectExpenseItem(false)} formId={formId} onSelect={handleAddExpenseItemRow} title="Добавление строки" sheet={sheetName} /> setIsOpenModalCreateProgram(false)} title={isProgramCreate ? "Создание программы" : "Создание проекта"} onCreate={isProgramCreate ? handleAddProgramRow : handleAddProjectRow} /> ); }; export default React.memo(RealtimeTable);