Compare commits

..

No commits in common. "905fa98ca5ff0151d42a0187b2eeb5ae15a74b1c" and "2343b5c9b1b8d50ea901df3c1af106a5a767c4f7" have entirely different histories.

7 changed files with 40 additions and 288 deletions

View File

@ -59,29 +59,13 @@ const highlightText = (text, searchQuery) => {
);
};
const INTEGER_COLUMN_IDS = new Set([
'data.header.num_group',
'data.nomenclature_group_id',
'data.header.item_id',
'data.article_id',
'data.code_razdela',
]);
// Функция для форматирования числа в финансовый формат
const formatNumber = (value, asInteger = false) => {
const formatNumber = (value) => {
if (value === null || value === undefined || value === '') return String(value || '');
const num = Number(value);
if (isNaN(num)) return String(value);
if (asInteger) {
return num.toLocaleString('ru-RU', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
useGrouping: true,
});
}
return num.toLocaleString('ru-RU', {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
@ -100,8 +84,7 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC
const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
if (isNumeric) {
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
textValue = formatNumber(cellValue, asInteger);
textValue = formatNumber(cellValue);
}
const highlightedContent = useMemo(() => {

View File

@ -1,148 +0,0 @@
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
const toLocalCoord = (value, scale) => value / scale;
const getPinnedClipLeft = (container, overlayParent, columnPinning, scale) => {
const pinnedIds = (columnPinning?.left || []).filter(Boolean);
if (!pinnedIds.length) return 0;
const parentRect = overlayParent.getBoundingClientRect();
let maxRight = 0;
for (const id of pinnedIds) {
const el = container.querySelector(`[data-col-select-id="${id}"]`);
if (!el) continue;
const rect = el.getBoundingClientRect();
maxRight = Math.max(maxRight, rect.right - parentRect.left);
}
return toLocalCoord(maxRight, scale);
};
const getOverlayRect = (container, overlayParent, selectedColumnId, columnPinning, scale) => {
const columnEl = container.querySelector(
`[data-col-select-id="${selectedColumnId}"]`,
);
if (!columnEl || !overlayParent) return null;
const bodyCell = container.querySelector(
`td[data-col-id="${selectedColumnId}"]`,
);
const parentRect = overlayParent.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const columnRect = columnEl.getBoundingClientRect();
const alignRect = bodyCell?.getBoundingClientRect() ?? columnRect;
let top = toLocalCoord(columnRect.top - parentRect.top, scale);
let left = toLocalCoord(alignRect.left - parentRect.left, scale);
let width = toLocalCoord(alignRect.width, scale);
const height = toLocalCoord(containerRect.bottom - columnRect.top, scale);
const pinnedIds = (columnPinning?.left || []).filter(Boolean);
const isSelectedPinned = pinnedIds.includes(selectedColumnId);
if (!isSelectedPinned && pinnedIds.length) {
const clipLeft = getPinnedClipLeft(container, overlayParent, columnPinning, scale);
const overlayRight = left + width;
if (overlayRight <= clipLeft) return null;
if (left < clipLeft) {
width = overlayRight - clipLeft;
left = clipLeft;
}
}
if (width <= 0 || height <= 0) return null;
return { top, left, width, height };
};
export const ColumnSelectionOverlay = ({
selectedColumnId,
containerRef,
scale = 1,
columnSizing,
columnPinning,
showColumnFilters,
}) => {
const [rect, setRect] = useState(null);
const [portalTarget, setPortalTarget] = useState(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const overlayParent = container.parentElement;
setPortalTarget(overlayParent);
}, [containerRef, selectedColumnId]);
useEffect(() => {
const container = containerRef.current;
const overlayParent = container?.parentElement;
if (!selectedColumnId || !container || !overlayParent) {
setRect(null);
return;
}
const updateRect = () => {
setRect(getOverlayRect(container, overlayParent, selectedColumnId, columnPinning, scale));
};
updateRect();
const scrollTargets = new Set([container]);
let scrollParent = container.parentElement;
for (let i = 0; i < 3 && scrollParent; i += 1) {
scrollTargets.add(scrollParent);
scrollParent = scrollParent.parentElement;
}
scrollTargets.forEach((target) => {
target.addEventListener('scroll', updateRect, { passive: true });
});
window.addEventListener('resize', updateRect);
const resizeObserver = new ResizeObserver(updateRect);
resizeObserver.observe(container);
resizeObserver.observe(overlayParent);
const header = container.querySelector('.custom-table-header');
if (header) {
resizeObserver.observe(header);
}
const tableBody = container.querySelector('.MuiTableBody-root');
if (tableBody) {
resizeObserver.observe(tableBody);
}
return () => {
scrollTargets.forEach((target) => {
target.removeEventListener('scroll', updateRect);
});
window.removeEventListener('resize', updateRect);
resizeObserver.disconnect();
};
}, [selectedColumnId, containerRef, scale, columnSizing, columnPinning, showColumnFilters]);
if (!rect || !portalTarget) return null;
return createPortal(
<div
style={{
position: 'absolute',
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
backgroundColor: 'rgba(88, 154, 47, 0.2)',
border: '1px solid #589A2F',
boxSizing: 'border-box',
pointerEvents: 'none',
zIndex: 1,
}}
/>,
portalTarget,
);
};

View File

@ -11,14 +11,13 @@ 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,
getTableHeadCellStyles,
getTablePaperStyles,
TABLE_ROW_HEIGHT,
} from './constants/tableConfig';
@ -30,7 +29,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction);
const [globalFilter, setGlobalFilter] = useState('');
const [showColumnFilters, setShowColumnFilters] = useState(false);
const [selectedColumnId, setSelectedColumnId] = useState();
const [selectedColumn, setSelectedColumn] = useState();
const [rowSelection, setRowSelection] = useState({});
const [isLoadingData, setIsLoadingData] = useState(false);
const [isPending, startTransition] = useTransition();
@ -103,26 +102,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
}));
}, [])
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 columns = useMemo(() => {
if (!columnsConfig || !columnsConfig.columns) return [];
@ -142,12 +121,11 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
console.error('Error creating columns:', error);
return [];
}
}, [columnsConfig, handleUpdateCell, handleClickRowCell]);
}, [columnsConfig, handleUpdateCell]);
const tableMeta = useMemo(() => ({
updateCell: handleUpdateCell,
selectedColumnId,
}), [handleUpdateCell, selectedColumnId]);
}), [handleUpdateCell]);
// Конфигурация таблицы
const tableConfig = useMemo(
@ -210,7 +188,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
muiTableHeadCellProps: {
sx: { boxSizing: 'border-box' },
},
muiTableBodyCellProps: getTableBodyCellProps,
muiTableBodyCellProps: getTableHeadCellStyles,
muiTableHeadProps: {
sx: {
display: 'table-header-group',
@ -323,8 +301,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
return (
<>
<SettingsPanel
selectedColumnId={selectedColumnId}
columnPinning={columnPinning}
selectedColumn={selectedColumn}
table={table}
columns={columns}
onPinColumn={handlePinColumn}
@ -356,15 +333,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
>
<MaterialReactTable table={table} />
<ColumnSelectionOverlay
selectedColumnId={selectedColumnId}
containerRef={containerRef}
scale={sizeMult}
columnSizing={columnSizing}
columnPinning={columnPinning}
showColumnFilters={showColumnFilters}
/>
{isTableLoading && (
<div
style={{
@ -386,8 +354,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
<TableHead
table={table}
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
selectedColumnId={selectedColumnId}
onColumnSelect={handleColumnSelect}
onHeaderSelect={setSelectedColumn}
selectHeader={selectedColumn}
onChangeWidth={handleSetColumnWidth}
isLoadingData={isLoadingData}
/>,

View File

@ -21,8 +21,7 @@ import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
import { exportSheet } from '../../../utils/exportForm';
const SettingPanel = ({
selectedColumnId,
columnPinning,
selectedColumn,
table,
columns,
onPinColumn,
@ -45,16 +44,12 @@ const SettingPanel = ({
const [selectedColumnIds, setSelectedColumnIds] = useState();
const [showColumnFilters, setShowColumnFilters] = useState(false);
// TODO: console.log(!!!) везде посомтреть
useEffect(() => {
if (!selectedColumnId) {
setIsPinned(false);
return;
}
const pinningColumn = columnPinning?.left || table.getState().columnPinning.left || [];
const pinned = pinningColumn.includes(selectedColumnId);
setIsPinned(pinned);
}, [selectedColumnId, columnPinning, table]);
if (!selectedColumn) return;
const pinningColumn = table.getState().columnPinning.left;
const isPinned = pinningColumn.includes(selectedColumn.id);
setIsPinned(isPinned);
}, [selectedColumn, table]);
useEffect(() => {
setSelectedColumnIds(
@ -84,7 +79,7 @@ const SettingPanel = ({
return (
<>
<Panel data-pin-panel>
<Panel>
<Stack direction="row" sx={{ gap: '1.25rem' }}>
<GroupByObject title="Отображение">
{/* <ColorPickerButton onColorApply={addColorForCells} /> */}
@ -93,22 +88,11 @@ const SettingPanel = ({
<IconButton
variant="outlined"
data-active={isPinned}
disabled={!selectedColumnId}
onMouseDown={(e) => e.stopPropagation()}
onClick={() => {
if (!selectedColumnId) {
console.warn('[ColumnPin] клик Pin: selectedColumnId пустой');
return;
}
console.log('[ColumnPin] клик Pin:', {
selectedColumnId,
isPinned,
action: isPinned ? 'unpin' : 'pin',
});
onClick={() =>
isPinned
? onUnpinColumn?.(selectedColumnId)
: onPinColumn?.(selectedColumnId);
}}
? onUnpinColumn?.(selectedColumn.id)
: onPinColumn?.(selectedColumn.id)
}
>
<Pin />
</IconButton>

View File

@ -2,12 +2,9 @@ import React from 'react';
import ColumnResizer from './ColumnResizer';
import { columnPinningDefault } from '../constants/columnConfig';
const getPinnedColumnIds = (table) =>
(table.getState().columnPinning.left || []).filter(Boolean);
const getSortedColumns = (table) => {
const allLeafColumns = table.getAllLeafColumns();
const pinnedColumns = getPinnedColumnIds(table);
const pinnedColumns = table.getState().columnPinning.left || [];
return [...allLeafColumns].sort((a, b) => {
const aIsPinned = pinnedColumns.includes(a.id);
@ -49,7 +46,7 @@ const getPinnedColumnsWidth = (header, pinningColumn) => {
const calculateLeftOffset = (table, columnOrHeader) => {
const columnId = getColumnId(columnOrHeader);
const pinnedColumns = getPinnedColumnIds(table);
const pinnedColumns = table.getState().columnPinning.left || [];
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
let leftOffset = 0;
@ -67,13 +64,13 @@ const calculateLeftOffset = (table, columnOrHeader) => {
};
const isColumnPin = (table, header) => {
const pinningColumn = getPinnedColumnIds(table);
const pinningColumn = table.getState().columnPinning.left;
const columnId = getColumnId(header);
const isPinned = pinningColumn.includes(columnId);
return isPinned;
};
const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
const ColumnNumbersRow = ({ table }) => {
const sortedColumns = getSortedColumns(table);
const tableState = table.getState();
const filteredColumns = sortedColumns.filter(
@ -98,38 +95,27 @@ const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
key={`col-number-${column.id}`}
column={column}
table={table}
selectedColumnId={selectedColumnId}
onColumnSelect={onColumnSelect}
/>
))}
</div>
);
};
const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => {
const ColumnNumberCell = ({ column, table }) => {
const isPinned = isColumnPin(table, column);
const allLeafColumns = table.getAllLeafColumns();
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
const leftOffset = calculateLeftOffset(table, column);
const selected = selectedColumnId === column.id;
return (
<div
key={`col-number-${column.id}`}
data-col-select-id={column.id}
onClick={(e) => {
e.stopPropagation();
onColumnSelect?.(column.id);
}}
onMouseDown={(e) => e.stopPropagation()}
style={{
width: column.getSize(),
minWidth: column.getSize(),
maxWidth: column.getSize(),
padding: '2px 6px',
textAlign: 'center',
backgroundColor: selected ? '#589A2F' : '#FBFBFB',
color: selected ? '#ffffff' : '#5E5E5E',
backgroundColor: '#e5e5e5',
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
@ -139,9 +125,7 @@ const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) =
borderBottom: 'none',
position: isPinned ? 'sticky' : 'relative',
left: isPinned ? leftOffset : 'auto',
zIndex: selected ? 16 : isPinned ? 20 : 'auto',
cursor: 'pointer',
userSelect: 'none',
zIndex: isPinned ? 20 : 'auto',
}}
>
{originalIndex}
@ -149,9 +133,9 @@ const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) =
);
};
const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => {
const column = header.column;
const pinningColumn = getPinnedColumnIds(table);
const pinningColumn = table.getState().columnPinning.left || [];
const isGroup = header.subHeaders?.length > 0;
const isPinned = isColumnPin(table, header);
const leftOffset = calculateLeftOffset(table, header);
@ -164,7 +148,7 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
const backgroundColor = customBgColor
? customBgColor
: 'rgba(243, 244, 246, 1)';
: (hightlight ? '#cbccce' : 'rgba(243, 244, 246, 1)');
const handleChangeWidth = (deltaWidth) => {
const size = header.getSize();
@ -180,7 +164,7 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
}
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
// Получаем общую ширину всех закрепленных колонок
const pinnedColumns = getPinnedColumnIds(table);
const pinnedColumns = table.getState().columnPinning.left || [];
let totalPinnedWidth = 0;
for (const pinnedId of pinnedColumns) {
const pinnedCol = table.getColumn(pinnedId);
@ -204,7 +188,6 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
textAlign: 'center',
fontWeight: 600,
fontSize: '12px',
lineHeight: 1,
backgroundColor: backgroundColor,
color: '#424242',
boxSizing: 'border-box',
@ -396,15 +379,13 @@ const FilterCell = ({ column, table }) => {
export const TableHead = ({
table,
selectedColumnId,
onColumnSelect,
onHeaderSelect,
selectHeader,
onChangeWidth,
isLoadingData,
}) => {
const handleHeaderClick = (header) => {
if (!header.subHeaders?.length && header.column?.id) {
onColumnSelect?.(header.column.id);
}
onHeaderSelect(header);
};
if (!isLoadingData) {
@ -421,15 +402,9 @@ export const TableHead = ({
}}
>
{/* Строка с номерами колонок */}
<ColumnNumbersRow
table={table}
selectedColumnId={selectedColumnId}
onColumnSelect={onColumnSelect}
/>
<ColumnNumbersRow table={table} />
{table.getState().showColumnFilters && (
<FilterRow table={table} />
)}
{table.getState().showColumnFilters && <FilterRow table={table} />}
{/* Существующие строки заголовков */}
{table.getHeaderGroups().map((headerGroup) => (
@ -438,6 +413,9 @@ export const TableHead = ({
<React.Fragment key={header.id}>
<HeaderCell
header={header}
hightlight={
selectHeader ? header.id == selectHeader.id : false
}
table={table}
onClick={handleHeaderClick}
onChangeWidth={onChangeWidth}

View File

@ -45,18 +45,6 @@ export const getTableHeadCellStyles = () => ({
},
});
export const getTableBodyCellProps = ({ column, table }) => {
const isPinned = (table.getState().columnPinning?.left || []).includes(column.id);
return {
'data-col-id': column.id,
sx: {
...getTableHeadCellStyles().sx,
...(isPinned && { zIndex: 5 }),
},
};
};
export const getTablePaperStyles = () => ({
sx: {
boxShadow: "none",

View File

@ -41,7 +41,6 @@ export const useColumnSettings = (tableKey) => {
}, []);
const handleUnpinColumn = useCallback((colId) => {
if (!colId) return;
if (columnPinningDefault.left.includes(colId)) return;
setColumnPinning((prev) => ({
...prev,