diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index 5f05400..2164513 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -59,13 +59,29 @@ 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) => { +const formatNumber = (value, asInteger = false) => { 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, @@ -84,7 +100,8 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== ''; if (isNumeric) { - textValue = formatNumber(cellValue); + const asInteger = INTEGER_COLUMN_IDS.has(column.id); + textValue = formatNumber(cellValue, asInteger); } const highlightedContent = useMemo(() => { diff --git a/web/src/components/RealtimeTable/ColumnSelectionOverlay/ColumnSelectionOverlay.jsx b/web/src/components/RealtimeTable/ColumnSelectionOverlay/ColumnSelectionOverlay.jsx new file mode 100644 index 0000000..5bed9f8 --- /dev/null +++ b/web/src/components/RealtimeTable/ColumnSelectionOverlay/ColumnSelectionOverlay.jsx @@ -0,0 +1,148 @@ +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( +
, + portalTarget, + ); +}; diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 6b37f2a..da518a6 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -11,13 +11,14 @@ 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, - getTableHeadCellStyles, + getTableBodyCellProps, getTablePaperStyles, TABLE_ROW_HEIGHT, } from './constants/tableConfig'; @@ -29,7 +30,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 [selectedColumn, setSelectedColumn] = useState(); + const [selectedColumnId, setSelectedColumnId] = useState(); const [rowSelection, setRowSelection] = useState({}); const [isLoadingData, setIsLoadingData] = useState(false); const [isPending, startTransition] = useTransition(); @@ -102,6 +103,26 @@ 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 []; @@ -121,11 +142,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { console.error('Error creating columns:', error); return []; } - }, [columnsConfig, handleUpdateCell]); + }, [columnsConfig, handleUpdateCell, handleClickRowCell]); const tableMeta = useMemo(() => ({ updateCell: handleUpdateCell, - }), [handleUpdateCell]); + selectedColumnId, + }), [handleUpdateCell, selectedColumnId]); // Конфигурация таблицы const tableConfig = useMemo( @@ -188,7 +210,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { muiTableHeadCellProps: { sx: { boxSizing: 'border-box' }, }, - muiTableBodyCellProps: getTableHeadCellStyles, + muiTableBodyCellProps: getTableBodyCellProps, muiTableHeadProps: { sx: { display: 'table-header-group', @@ -301,7 +323,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { return ( <>