diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
index 96970b1..f92fbd3 100644
--- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
+++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
@@ -110,7 +110,7 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
const cleanQueries = searchQueries.filter(Boolean);
if (cleanQueries.length === 0) return displayValue;
- const searchStr = String(originalValue ?? displayValue);
+ const searchStr = String(displayValue ?? originalValue);
const escapedQueries = cleanQueries.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx
index c55aed6..4ca025c 100644
--- a/web/src/components/RealtimeTable/RealtimeTable.jsx
+++ b/web/src/components/RealtimeTable/RealtimeTable.jsx
@@ -17,7 +17,7 @@ import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal';
import { SelectVspModal } from './Modals/SelectVspModal';
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig';
-import { useRealtimeActions } from './contexts/RealtimeContext';
+import { useRealtimeActions, useVspOptions } from './contexts/RealtimeContext';
import { useColumnSettings } from './hooks/useColumnSettings';
import { useHeaderPortal } from './hooks/useHeaderPortal';
import { useTableScale } from './hooks/useTableScale';
@@ -39,15 +39,15 @@ const ROW_VIRTUALIZER_OPTIONS = {
};
const getColumnVirtualizerOptions = ({ table }) => {
- const orderedVisibleColumns = [
- ...table.getLeftVisibleLeafColumns(),
- ...table.getCenterVisibleLeafColumns(),
- ...table.getRightVisibleLeafColumns(),
- ];
+ const orderedVisibleColumns = table.getVisibleLeafColumns();
const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150;
+ const scrollPaddingStart = table.getLeftVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
+ const scrollPaddingEnd = table.getRightVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
return {
overscan: 10,
+ scrollPaddingStart,
+ scrollPaddingEnd,
estimateSize: getColumnSize,
measureElement: (element) => {
if (!element) return 150;
@@ -59,6 +59,18 @@ const getColumnVirtualizerOptions = ({ table }) => {
const hasVspDropdown = (columns) =>
columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns)));
+const normalizeFilterValue = (value) =>
+ String(value ?? '')
+ .trim()
+ .toLocaleLowerCase('ru-RU');
+
+const doesCellMatchFilter = (rawValue, columnDef, query, vspRegistrationById) => {
+ if (normalizeFilterValue(rawValue).includes(query)) return true;
+ if (columnDef?.editType !== 'vsp_dropdown') return false;
+
+ return vspRegistrationById.get(Number(rawValue))?.includes(query) ?? false;
+};
+
const tableContentStyle = {
position: 'relative',
width: '100%',
@@ -79,6 +91,7 @@ const loadingOverlayStyle = {
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const { user } = useAuth();
+ const vspOptions = useVspOptions();
const userRoleId = user?.role_id;
const {
data,
@@ -87,6 +100,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
editingCells: dataEditingCells,
} = useRealtimeData(formId, sheetName, direction, formType, year);
const [globalFilter, setGlobalFilter] = useState('');
+ const [searchMatchIndex, setSearchMatchIndex] = useState(-1);
+ const [activeSearchMatch, setActiveSearchMatch] = useState(null);
+ const [pendingSearchDirection, setPendingSearchDirection] = useState(0);
const [showColumnFilters, setShowColumnFilters] = useState(false);
const [selectedColumnId, setSelectedColumnId] = useState();
const [rowSelection, setRowSelection] = useState({});
@@ -120,9 +136,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const handleGlobalFilterChange = useMemo(
() =>
- debounce((value) => {
+ debounce((valueOrUpdater) => {
startTransition(() => {
- setGlobalFilter(value);
+ setGlobalFilter((currentValue) => (typeof valueOrUpdater === 'function' ? valueOrUpdater(currentValue) : valueOrUpdater));
});
}, 300),
[startTransition],
@@ -300,6 +316,23 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
[handleUpdateCell],
);
+ const vspRegistrationById = useMemo(
+ () => new Map(vspOptions.map((vsp) => [Number(vsp.id), normalizeFilterValue(vsp.registration_number)])),
+ [vspOptions],
+ );
+
+ const globalFilterFn = useCallback(
+ (row, columnId, filterValue) => {
+ const query = normalizeFilterValue(filterValue);
+ if (!query) return true;
+
+ const rawValue = row.getValue(columnId);
+ const column = row.getAllCells().find((cell) => cell.column.id === columnId)?.column;
+ return doesCellMatchFilter(rawValue, column?.columnDef, query, vspRegistrationById);
+ },
+ [vspRegistrationById],
+ );
+
const createTableConfig = ({
columns,
data,
@@ -317,8 +350,11 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setExpanded,
editingCell,
contextStartEditing,
+ globalFilterFn,
+ activeSearchMatch,
}) => ({
...BASE_TABLE_CONFIG,
+ globalFilterFn,
columns,
data,
enableRowVirtualization: true,
@@ -355,7 +391,21 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
muiTableHeadCellProps: {
sx: { boxSizing: 'border-box' },
},
- muiTableBodyCellProps: getTableBodyCellProps,
+ muiTableBodyCellProps: (props) => {
+ const cellProps = getTableBodyCellProps(props);
+ const isActiveMatch = activeSearchMatch?.rowId === props.row.id && activeSearchMatch?.columnId === props.column.id;
+
+ return {
+ ...cellProps,
+ sx: {
+ ...cellProps.sx,
+ ...(isActiveMatch && {
+ zIndex: 7,
+ boxShadow: 'inset 0 0 0 3px #1976d2',
+ }),
+ },
+ };
+ },
muiTableHeadProps: {
sx: {
display: 'table-header-group',
@@ -406,6 +456,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setExpanded,
editingCell,
contextStartEditing,
+ globalFilterFn,
+ activeSearchMatch,
}),
[
columns,
@@ -422,6 +474,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setColumnPinning,
editingCell,
expanded,
+ globalFilterFn,
+ activeSearchMatch,
],
);
@@ -431,31 +485,108 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const handleNavigateToColumn = useCallback(
(columnId) => {
if (!columnId) return;
+ console.log(columnId);
+ console.log(columnId);
+ console.log(columnId);
+ console.log(columnId);
setSelectedColumnId(columnId);
+ const pinnedIds = new Set([...(columnPinning?.left || []), ...(columnPinning?.right || [])]);
+ if (pinnedIds.has(columnId)) return;
+
+ const visibleColumns = table.getVisibleLeafColumns();
+ const columnIndex = visibleColumns.findIndex((column) => column.id === columnId);
+ if (columnIndex < 0) return;
+
+ const columnVirtualizer = columnVirtualizerRef.current;
const container = containerRef.current;
if (!container) return;
- const pinnedIds = new Set(columnPinning?.left || []);
- if (pinnedIds.has(columnId)) return;
-
- const centerColumns = table.getVisibleLeafColumns().filter((column) => !pinnedIds.has(column.id));
-
- let offset = 0;
- for (const column of centerColumns) {
- if (column.id === columnId) break;
- offset += column.getSize();
- }
+ const leftPinnedSize = table.getLeftVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
+ const calculatedStart = visibleColumns.slice(0, columnIndex).reduce((offset, column) => offset + column.getSize(), 0);
+ const measurement = columnVirtualizer?.measurementsCache?.[columnIndex];
+ const targetStart = measurement?.start ?? calculatedStart;
+ const targetOffset = Math.max(0, targetStart - leftPinnedSize);
container.scrollTo({
- left: Math.max(0, offset - 40),
- behavior: 'smooth',
+ left: targetOffset,
});
},
[columnPinning, containerRef, table],
);
+ const searchMatches = useMemo(() => {
+ const query = normalizeFilterValue(globalFilter);
+ if (!query) return [];
+
+ const matches = [];
+ for (const [rowIndex, row] of table.getRowModel().rows.entries()) {
+ for (const cell of row.getVisibleCells()) {
+ if (doesCellMatchFilter(cell.getValue(), cell.column.columnDef, query, vspRegistrationById)) {
+ matches.push({ rowId: row.id, rowIndex, columnId: cell.column.id });
+ }
+ }
+ }
+
+ return matches;
+ }, [data, globalFilter, table, vspRegistrationById, columnVisibility]);
+
+ const navigateToSearchMatch = useCallback(
+ (direction) => {
+ if (!searchMatches.length) return;
+
+ setSearchMatchIndex((currentIndex) => {
+ const startIndex = currentIndex < 0 ? (direction > 0 ? -1 : 0) : currentIndex;
+ const nextIndex = (startIndex + direction + searchMatches.length) % searchMatches.length;
+ const match = searchMatches[nextIndex];
+
+ setActiveSearchMatch(match);
+ setRowSelection({ [match.rowId]: true });
+ console.log(match.columnId);
+ handleNavigateToColumn(match.columnId);
+ requestAnimationFrame(() => {
+ rowVirtualizerRef.current?.scrollToIndex?.(match.rowIndex < 5 ? match.rowIndex : match.rowIndex + 5, { align: 'auto', behavior: 'smooth' });
+ });
+
+ return nextIndex;
+ });
+ },
+ [handleNavigateToColumn, searchMatches],
+ );
+
+ const handleSearchKeyDown = useCallback(
+ (event) => {
+ if (event.key !== 'Enter') return;
+
+ event.preventDefault();
+ const direction = event.shiftKey ? -1 : 1;
+ const inputValue = event.target.value;
+
+ if (normalizeFilterValue(inputValue) !== normalizeFilterValue(globalFilter)) {
+ handleGlobalFilterChange.clear();
+ setGlobalFilter(inputValue);
+ setPendingSearchDirection(direction);
+ return;
+ }
+
+ navigateToSearchMatch(direction);
+ },
+ [globalFilter, handleGlobalFilterChange, navigateToSearchMatch],
+ );
+
+ useEffect(() => {
+ setSearchMatchIndex(-1);
+ setActiveSearchMatch(null);
+ }, [globalFilter]);
+
+ useEffect(() => {
+ if (!pendingSearchDirection) return;
+
+ navigateToSearchMatch(pendingSearchDirection);
+ setPendingSearchDirection(0);
+ }, [navigateToSearchMatch, pendingSearchDirection]);
+
useEffect(() => {
if (!dataEditingCells?.line_id) {
setEditingCell(null);
@@ -623,6 +754,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
columnsCurStage={columnsCurStage}
onChangeSizeMult={setSizeMult}
onGlobalFilterChange={handleGlobalFilterChange}
+ globalFilter={globalFilter}
+ onSearchKeyDown={handleSearchKeyDown}
+ onSearchNext={() => navigateToSearchMatch(1)}
+ onSearchPrevious={() => navigateToSearchMatch(-1)}
+ searchMatchCount={searchMatches.length}
+ searchMatchIndex={searchMatchIndex}
sizeMult={sizeMult}
onChangeShowColumnFilters={setShowColumnFilters}
showColumnFilters={showColumnFilters}
diff --git a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx
index 092195e..57025c7 100644
--- a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx
+++ b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx
@@ -32,6 +32,32 @@ const controlSx = {
};
const searchSx = { height: '2.5rem' };
+const searchNavigationSx = {
+ display: 'inline-flex',
+ alignItems: 'center',
+ height: '2rem',
+ border: '1px solid rgba(0, 0, 0, 0.12)',
+ borderRadius: '0.4rem',
+ overflow: 'hidden',
+ backgroundColor: '#fff',
+};
+const searchNavigationButtonSx = {
+ width: '1.65rem',
+ height: '2rem',
+ minWidth: '1.65rem',
+ padding: 0,
+ borderRadius: 0,
+ fontSize: '0.95rem',
+};
+const searchMatchCounterSx = {
+ minWidth: '2.7rem',
+ padding: '0 0.3rem',
+ fontSize: '0.7rem',
+ lineHeight: 1,
+ textAlign: 'center',
+ whiteSpace: 'nowrap',
+ color: 'rgba(0, 0, 0, 0.65)',
+};
const exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' };
const SettingPanel = ({
@@ -46,6 +72,12 @@ const SettingPanel = ({
columnsCurStage,
onChangeSizeMult,
onGlobalFilterChange,
+ globalFilter,
+ onSearchKeyDown,
+ onSearchNext,
+ onSearchPrevious,
+ searchMatchCount,
+ searchMatchIndex,
sizeMult,
onChangeShowColumnFilters,
showColumnFilters,
@@ -218,7 +250,40 @@ const SettingPanel = ({
-
+
+
+
+
+
+ ↑
+
+
+
+
+ {searchMatchCount ? `${Math.max(searchMatchIndex + 1, 1)}/${searchMatchCount}` : '0/0'}
+
+
+
+
+ ↓
+
+
+
+