fixies-3-form #105
@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo, useCallback } from 'react';
|
import React, { useMemo, useCallback } from 'react';
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
import { useCellLock } from '../../contexts/RealtimeContext';
|
||||||
|
|
||||||
// Константы вне компонента
|
// Константы вне компонента
|
||||||
const BASE_CELL_STYLES = {
|
const BASE_CELL_STYLES = {
|
||||||
@ -136,35 +136,20 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const CellComponent = ({
|
const CellComponent = ({ cell, row, column, onClick, globalFilter, columnFilter, backgroundColor, isEditable, isInvalid, vspOptions }) => {
|
||||||
cell,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
onClick,
|
|
||||||
globalFilter,
|
|
||||||
columnFilter,
|
|
||||||
backgroundColor,
|
|
||||||
color,
|
|
||||||
isEditable,
|
|
||||||
isUpdating,
|
|
||||||
onCellNumberClick,
|
|
||||||
isInvalid,
|
|
||||||
vspOptions,
|
|
||||||
}) => {
|
|
||||||
const { lockedCells } = useRealtime();
|
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
const cellKey = `${row.id}_${column.id}`;
|
||||||
const isLocked = lockedCells.includes(cellKey);
|
const isLocked = useCellLock(cellKey);
|
||||||
|
|
||||||
const isVspDropdown = column?.columnDef?.editType === 'vsp_dropdown';
|
const isVspDropdown = column?.columnDef?.editType === 'vsp_dropdown';
|
||||||
|
|
||||||
// Мемоизация значения
|
// Мемоизация значения
|
||||||
const { rawValue, displayValue, isNumeric } = useMemo(() => {
|
const { rawValue, displayValue } = useMemo(() => {
|
||||||
const value = cell.getValue();
|
const value = cell.getValue();
|
||||||
const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value));
|
const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value));
|
||||||
let display = String(value || '');
|
let display = String(value || '');
|
||||||
|
|
||||||
if (isVspDropdown && value != null) {
|
if (isVspDropdown && value != null) {
|
||||||
const vsp = vspOptions?.find(v => v.id === Number(value));
|
const vsp = vspOptions?.find((v) => v.id === Number(value));
|
||||||
if (vsp) {
|
if (vsp) {
|
||||||
return { rawValue: value, displayValue: vsp.registration_number, isNumeric: false };
|
return { rawValue: value, displayValue: vsp.registration_number, isNumeric: false };
|
||||||
}
|
}
|
||||||
@ -214,7 +199,7 @@ const CellComponent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||||
<div type="button" className='cell' style={cellStyles} onClick={handleClick}>
|
<div type='button' className='cell' style={cellStyles} onClick={handleClick}>
|
||||||
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
||||||
{lockBadge}
|
{lockBadge}
|
||||||
</div>
|
</div>
|
||||||
@ -232,10 +217,8 @@ const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
|||||||
prevProps.backgroundColor === nextProps.backgroundColor &&
|
prevProps.backgroundColor === nextProps.backgroundColor &&
|
||||||
prevProps.isInvalid === nextProps.isInvalid &&
|
prevProps.isInvalid === nextProps.isInvalid &&
|
||||||
prevProps.isEditable === nextProps.isEditable &&
|
prevProps.isEditable === nextProps.isEditable &&
|
||||||
prevProps.isUpdating === nextProps.isUpdating &&
|
|
||||||
prevProps.onCellNumberClick === nextProps.onCellNumberClick &&
|
|
||||||
prevProps.vspOptions === nextProps.vspOptions
|
prevProps.vspOptions === nextProps.vspOptions
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Cell;
|
export default Cell;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
import { useRealtimeActions } from '../../contexts/RealtimeContext';
|
||||||
import { calculateFormula, extractFormula, formatNumber, isFormula } from '../../utils/formulaUtils';
|
import { calculateFormula, extractFormula, formatNumber, isFormula } from '../../utils/formulaUtils';
|
||||||
import { loadFromStorage, saveToStorage } from '../../utils/localStorageUtils';
|
import { loadFromStorage, saveToStorage } from '../../utils/localStorageUtils';
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ const TEXTAREA_STYLES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
||||||
const { endEditing: contextEndEditing } = useRealtime();
|
const { endEditing: contextEndEditing } = useRealtimeActions();
|
||||||
const storageKey = `formula_${tableId}_${cellId}`;
|
const storageKey = `formula_${tableId}_${cellId}`;
|
||||||
|
|
||||||
const [state, setState] = useState(() => ({
|
const [state, setState] = useState(() => ({
|
||||||
@ -91,7 +91,7 @@ const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, ta
|
|||||||
const resize = () => {
|
const resize = () => {
|
||||||
el.style.minHeight = '3rem';
|
el.style.minHeight = '3rem';
|
||||||
el.style.height = '3rem';
|
el.style.height = '3rem';
|
||||||
el.style.height = el.scrollHeight + 'px';
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
el.style.maxHeight = '180px';
|
el.style.maxHeight = '180px';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,111 +1,120 @@
|
|||||||
import React, { memo, useEffect, useRef, useLayoutEffect, useState, useCallback, useMemo } from 'react';
|
import { FormControl, MenuItem, Select } from '@mui/material';
|
||||||
import { FormControl, Select, MenuItem } from '@mui/material';
|
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
import { useRealtimeActions, useVspOptions } from '../../contexts/RealtimeContext';
|
||||||
|
|
||||||
const EDITOR_STYLES = {
|
const EDITOR_STYLES = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
zIndex: 12,
|
zIndex: 12,
|
||||||
top: 0,
|
top: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const VspDropdownEditCell = memo(({ refCell, onChange, disabled, value, table }) => {
|
const VspDropdownEditCell = memo(({ refCell, onChange, disabled, value, table }) => {
|
||||||
const { endEditing: contextEndEditing, vspOptions } = useRealtime();
|
const { endEditing: contextEndEditing } = useRealtimeActions();
|
||||||
|
const vspOptions = useVspOptions();
|
||||||
|
|
||||||
const [selectedValue, setSelectedValue] = useState(() => value ?? '');
|
const [selectedValue, setSelectedValue] = useState(() => value ?? '');
|
||||||
const [position, setPosition] = useState();
|
const [position, setPosition] = useState();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const refFinished = useRef(false);
|
const refFinished = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}, 80);
|
}, 80);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!refCell?.current) return;
|
if (!refCell?.current) return;
|
||||||
|
|
||||||
const td = refCell.current.offsetParent;
|
const td = refCell.current.offsetParent;
|
||||||
const tr = td?.offsetParent;
|
const tr = td?.offsetParent;
|
||||||
|
|
||||||
if (td && tr) {
|
if (td && tr) {
|
||||||
setPosition({
|
setPosition({
|
||||||
left: td.offsetLeft,
|
left: td.offsetLeft,
|
||||||
width: td.offsetWidth,
|
width: td.offsetWidth,
|
||||||
translateY: tr.style.transform || '',
|
translateY: tr.style.transform || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [refCell]);
|
}, [refCell]);
|
||||||
|
|
||||||
const finishEditing = useCallback(() => {
|
const finishEditing = useCallback(() => {
|
||||||
if (refFinished.current) return;
|
if (refFinished.current) return;
|
||||||
refFinished.current = true;
|
refFinished.current = true;
|
||||||
table.setEditingCell(null);
|
table.setEditingCell(null);
|
||||||
const editingCell = table.getState().editingCell;
|
const editingCell = table.getState().editingCell;
|
||||||
if (editingCell) {
|
if (editingCell) {
|
||||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||||
}
|
}
|
||||||
}, [table, contextEndEditing]);
|
}, [table, contextEndEditing]);
|
||||||
|
|
||||||
const handleSelectChange = useCallback((e) => {
|
const handleSelectChange = useCallback(
|
||||||
const val = e.target.value;
|
(e) => {
|
||||||
if (val == null || val === '') return;
|
const val = e.target.value;
|
||||||
refFinished.current = true;
|
if (val == null || val === '') return;
|
||||||
setSelectedValue(val);
|
refFinished.current = true;
|
||||||
onChange?.(val);
|
setSelectedValue(val);
|
||||||
const editingCell = table.getState().editingCell;
|
onChange?.(val);
|
||||||
if (editingCell) {
|
const editingCell = table.getState().editingCell;
|
||||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
if (editingCell) {
|
||||||
}
|
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||||
}, [onChange, table, contextEndEditing]);
|
}
|
||||||
|
},
|
||||||
|
[onChange, table, contextEndEditing],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectClose = useCallback((_event, reason) => {
|
const handleSelectClose = useCallback(
|
||||||
if (reason === 'selectOption') return;
|
(_event, reason) => {
|
||||||
finishEditing();
|
if (reason === 'selectOption') return;
|
||||||
}, [finishEditing]);
|
finishEditing();
|
||||||
|
},
|
||||||
|
[finishEditing],
|
||||||
|
);
|
||||||
|
|
||||||
const editorStyles = useMemo(() => ({
|
const editorStyles = useMemo(
|
||||||
...EDITOR_STYLES,
|
() => ({
|
||||||
left: position?.left || 0,
|
...EDITOR_STYLES,
|
||||||
width: position?.width || 0,
|
left: position?.left || 0,
|
||||||
transform: position?.translateY || 0,
|
width: position?.width || 0,
|
||||||
}), [position]);
|
transform: position?.translateY || 0,
|
||||||
|
}),
|
||||||
|
[position],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{position &&
|
{position && (
|
||||||
<div style={editorStyles}>
|
<div style={editorStyles}>
|
||||||
<FormControl fullWidth size="small">
|
<FormControl fullWidth size='small'>
|
||||||
<Select
|
<Select
|
||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={handleSelectClose}
|
onClose={handleSelectClose}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
|
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
|
||||||
transformOrigin: { vertical: 'top', horizontal: 'left' },
|
transformOrigin: { vertical: 'top', horizontal: 'left' },
|
||||||
}}
|
}}>
|
||||||
>
|
<MenuItem value='' disabled>
|
||||||
<MenuItem value="" disabled>
|
Выберите ВСП
|
||||||
Выберите ВСП
|
</MenuItem>
|
||||||
</MenuItem>
|
{vspOptions.map((option) => (
|
||||||
{vspOptions.map((option) => (
|
<MenuItem key={option.id} value={option.id}>
|
||||||
<MenuItem key={option.id} value={option.id}>
|
{option.registration_number}
|
||||||
{option.registration_number}
|
</MenuItem>
|
||||||
</MenuItem>
|
))}
|
||||||
))}
|
</Select>
|
||||||
</Select>
|
</FormControl>
|
||||||
</FormControl>
|
</div>
|
||||||
</div>
|
)}
|
||||||
}
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
VspDropdownEditCell.displayName = 'VspDropdownEditCell';
|
VspDropdownEditCell.displayName = 'VspDropdownEditCell';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
const toLocalCoord = (value, scale) => value / scale;
|
const toLocalCoord = (value, scale) => value / scale;
|
||||||
@ -54,7 +54,7 @@ const getOverlayRect = (container, overlayParent, selectedColumnId, columnPinnin
|
|||||||
return { top, left, width, height };
|
return { top, left, width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ColumnSelectionOverlay = ({ selectedColumnId, containerRef, scale = 1, columnSizing, columnPinning, showColumnFilters }) => {
|
const ColumnSelectionOverlayComponent = ({ selectedColumnId, containerRef, scale = 1, columnSizing, columnPinning, showColumnFilters }) => {
|
||||||
const [rect, setRect] = useState(null);
|
const [rect, setRect] = useState(null);
|
||||||
const [portalTarget, setPortalTarget] = useState(null);
|
const [portalTarget, setPortalTarget] = useState(null);
|
||||||
|
|
||||||
@ -135,3 +135,5 @@ export const ColumnSelectionOverlay = ({ selectedColumnId, containerRef, scale =
|
|||||||
portalTarget,
|
portalTarget,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ColumnSelectionOverlay = memo(ColumnSelectionOverlayComponent);
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Box, Divider, IconButton, Stack } from '@mui/material';
|
import { Box, Divider, IconButton, Stack } from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons';
|
import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons';
|
||||||
import { HeaderContainer, ModalBackdrop, ModalContainer, ModalTitle } from '../../common/Modal/ModalStyled';
|
import { HeaderContainer, ModalBackdrop, ModalContainer, ModalTitle } from '../../common/Modal/ModalStyled';
|
||||||
import { StyledTextField } from '../../common/StyledTextField';
|
import { StyledTextField } from '../../common/StyledTextField';
|
||||||
import { Cancel } from '../../common/icons/icons';
|
import { Cancel } from '../../common/icons/icons';
|
||||||
|
|
||||||
export const CreateProgramModal = ({ isOpen, onClose, onCreate, title = '', isSaving = false, sheet }) => {
|
const CreateProgramModalComponent = ({ isOpen, onClose, onCreate, title = '', isSaving = false }) => {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -36,7 +36,7 @@ export const CreateProgramModal = ({ isOpen, onClose, onCreate, title = '', isSa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
||||||
<ModalContainer height={'max-content'}>
|
<ModalContainer height='max-content'>
|
||||||
<HeaderContainer>
|
<HeaderContainer>
|
||||||
<Stack direction='row' spacing={2} sx={{ alignItems: 'center' }}>
|
<Stack direction='row' spacing={2} sx={{ alignItems: 'center' }}>
|
||||||
<ModalTitle>{title}</ModalTitle>
|
<ModalTitle>{title}</ModalTitle>
|
||||||
@ -50,7 +50,7 @@ export const CreateProgramModal = ({ isOpen, onClose, onCreate, title = '', isSa
|
|||||||
<Divider sx={{ marginBottom: '0.75rem' }} />
|
<Divider sx={{ marginBottom: '0.75rem' }} />
|
||||||
|
|
||||||
{/* <Input */}
|
{/* <Input */}
|
||||||
<StyledTextField label={'Название программы'} onChange={(e) => setName(e.target.value)}></StyledTextField>
|
<StyledTextField label='Название программы' onChange={(e) => setName(e.target.value)} />
|
||||||
<Stack
|
<Stack
|
||||||
direction='row'
|
direction='row'
|
||||||
spacing={0.75}
|
spacing={0.75}
|
||||||
@ -72,3 +72,5 @@ export const CreateProgramModal = ({ isOpen, onClose, onCreate, title = '', isSa
|
|||||||
</ModalBackdrop>
|
</ModalBackdrop>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CreateProgramModal = memo(CreateProgramModalComponent);
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { Autocomplete, Box, Divider, FormControl, IconButton, Stack, TextField } from '@mui/material';
|
import { Autocomplete, Box, Divider, FormControl, IconButton, Stack, TextField } from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { ExpenseItemApi } from '../../../api/expense-item';
|
import { ExpenseItemApi } from '../../../api/expense-item';
|
||||||
import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons';
|
import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons';
|
||||||
import { HeaderContainer, ModalBackdrop, ModalContainer, ModalTitle } from '../../common/Modal/ModalStyled';
|
import { HeaderContainer, ModalBackdrop, ModalContainer, ModalTitle } from '../../common/Modal/ModalStyled';
|
||||||
import { Cancel } from '../../common/icons/icons';
|
import { Cancel } from '../../common/icons/icons';
|
||||||
|
|
||||||
export const SelectExpenseItemModal = ({ isOpen, onClose, onSelect, formId, title = '', isSaving = false, sheet }) => {
|
const SelectExpenseItemModalComponent = ({ isOpen, onClose, onSelect, formId, title = '', isSaving = false, sheet }) => {
|
||||||
const [selectedValue, setSelectedValue] = useState('');
|
const [selectedValue, setSelectedValue] = useState('');
|
||||||
const [expenseItemOptions, setExpenseItemOption] = useState([]);
|
const [expenseItemOptions, setExpenseItemOption] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formId === null) return;
|
if (!isOpen || formId == null) return;
|
||||||
ExpenseItemApi.getAll({ r_start: true, sheet: sheet }).then((data) => {
|
ExpenseItemApi.getAll({ r_start: true, sheet: sheet }).then((data) => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setExpenseItemOption(data.result);
|
setExpenseItemOption(data.result);
|
||||||
@ -19,7 +19,7 @@ export const SelectExpenseItemModal = ({ isOpen, onClose, onSelect, formId, titl
|
|||||||
}
|
}
|
||||||
toast.error('Ошибка получения статья расходов');
|
toast.error('Ошибка получения статья расходов');
|
||||||
});
|
});
|
||||||
}, [formId]);
|
}, [formId, isOpen, sheet]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -49,7 +49,7 @@ export const SelectExpenseItemModal = ({ isOpen, onClose, onSelect, formId, titl
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
||||||
<ModalContainer height={'max-content'}>
|
<ModalContainer height='max-content'>
|
||||||
<HeaderContainer>
|
<HeaderContainer>
|
||||||
<Stack direction='row' spacing={2} sx={{ alignItems: 'center' }}>
|
<Stack direction='row' spacing={2} sx={{ alignItems: 'center' }}>
|
||||||
<ModalTitle>{title}</ModalTitle>
|
<ModalTitle>{title}</ModalTitle>
|
||||||
@ -72,7 +72,7 @@ export const SelectExpenseItemModal = ({ isOpen, onClose, onSelect, formId, titl
|
|||||||
getOptionLabel={(option) => option.name || ''}
|
getOptionLabel={(option) => option.name || ''}
|
||||||
getOptionKey={(option) => option.id}
|
getOptionKey={(option) => option.id}
|
||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
onChange={(event, newValue) => {
|
onChange={(_event, newValue) => {
|
||||||
setSelectedValue(newValue);
|
setSelectedValue(newValue);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
@ -118,3 +118,5 @@ export const SelectExpenseItemModal = ({ isOpen, onClose, onSelect, formId, titl
|
|||||||
</ModalBackdrop>
|
</ModalBackdrop>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SelectExpenseItemModal = memo(SelectExpenseItemModalComponent);
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { Box, Divider, FormControl, IconButton, MenuItem, Select, Stack } from '@mui/material';
|
import { Box, Divider, FormControl, IconButton, MenuItem, Select, Stack } from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { DictVspApi } from '../../../api/dict-vsp';
|
import { DictVspApi } from '../../../api/dict-vsp';
|
||||||
import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons';
|
import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons';
|
||||||
import { HeaderContainer, ModalBackdrop, ModalContainer, ModalTitle } from '../../common/Modal/ModalStyled';
|
import { HeaderContainer, ModalBackdrop, ModalContainer, ModalTitle } from '../../common/Modal/ModalStyled';
|
||||||
import { Cancel } from '../../common/icons/icons';
|
import { Cancel } from '../../common/icons/icons';
|
||||||
|
|
||||||
export const SelectVspModal = ({ isOpen, onClose, onSelect, formId, title = 'Выбор ВСП', isSaving = false }) => {
|
const SelectVspModalComponent = ({ isOpen, onClose, onSelect, formId, title = 'Выбор ВСП', isSaving = false }) => {
|
||||||
const [selectedValue, setSelectedValue] = useState('');
|
const [selectedValue, setSelectedValue] = useState('');
|
||||||
const [vspOptions, setVspOption] = useState([]);
|
const [vspOptions, setVspOption] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formId === null) return;
|
if (!isOpen || formId == null) return;
|
||||||
DictVspApi.getDropdownVsp({ form_id: formId }).then((data) => {
|
DictVspApi.getDropdownVsp({ form_id: formId }).then((data) => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setVspOption(data.result);
|
setVspOption(data.result);
|
||||||
@ -19,7 +19,7 @@ export const SelectVspModal = ({ isOpen, onClose, onSelect, formId, title = 'В
|
|||||||
}
|
}
|
||||||
toast.error('Ошибка получения ВСП');
|
toast.error('Ошибка получения ВСП');
|
||||||
});
|
});
|
||||||
}, [formId]);
|
}, [formId, isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -49,7 +49,7 @@ export const SelectVspModal = ({ isOpen, onClose, onSelect, formId, title = 'В
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
||||||
<ModalContainer height={'max-content'}>
|
<ModalContainer height='max-content'>
|
||||||
<HeaderContainer>
|
<HeaderContainer>
|
||||||
<Stack direction='row' spacing={2} sx={{ alignItems: 'center' }}>
|
<Stack direction='row' spacing={2} sx={{ alignItems: 'center' }}>
|
||||||
<ModalTitle>{title}</ModalTitle>
|
<ModalTitle>{title}</ModalTitle>
|
||||||
@ -99,3 +99,5 @@ export const SelectVspModal = ({ isOpen, onClose, onSelect, formId, title = 'В
|
|||||||
</ModalBackdrop>
|
</ModalBackdrop>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SelectVspModal = memo(SelectVspModalComponent);
|
||||||
|
|||||||
@ -1,70 +1,110 @@
|
|||||||
|
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||||
import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react';
|
import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
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 { ColumnSelectionOverlay } from './ColumnSelectionOverlay/ColumnSelectionOverlay';
|
||||||
|
import SettingsPanel from './SettingPanel/SettingPanel';
|
||||||
|
import { TableHead } from './TableHead/TableHead';
|
||||||
|
import useRealtimeData from './hooks/useRealtimeData';
|
||||||
|
import { Cell, EditCell } from './index';
|
||||||
|
import { getTableColumns } from './tableColumns';
|
||||||
|
|
||||||
import { useColumnSettings } from './hooks/useColumnSettings';
|
|
||||||
import { useTableScale } from './hooks/useTableScale';
|
|
||||||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
|
||||||
import { useValidationRules } from './hooks/useValidationRules';
|
|
||||||
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 { 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 { debounce } from '@mui/material';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
|
import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
|
||||||
|
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 { useColumnSettings } from './hooks/useColumnSettings';
|
||||||
|
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||||||
|
import { useTableScale } from './hooks/useTableScale';
|
||||||
|
import { useValidationRules } from './hooks/useValidationRules';
|
||||||
|
import { getRowId } from './utils/rowUtils';
|
||||||
|
|
||||||
import { FormsNoCanAddRow } from './constants/formConfig';
|
|
||||||
import { DictVspApi } from '../../api/dict-vsp';
|
import { DictVspApi } from '../../api/dict-vsp';
|
||||||
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
|
import { FormsNoCanAddRow } from './constants/formConfig';
|
||||||
|
|
||||||
|
const CONFIG_CACHE = new Map();
|
||||||
|
|
||||||
|
const ROW_VIRTUALIZER_OPTIONS = {
|
||||||
|
overscan: 5,
|
||||||
|
scrollPaddingStart: 0,
|
||||||
|
scrollPaddingEnd: 0,
|
||||||
|
estimateSize: () => TABLE_ROW_HEIGHT,
|
||||||
|
measureElement: (element) => element?.offsetHeight || TABLE_ROW_HEIGHT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnVirtualizerOptions = ({ table }) => {
|
||||||
|
const orderedVisibleColumns = [
|
||||||
|
...table.getLeftVisibleLeafColumns(),
|
||||||
|
...table.getCenterVisibleLeafColumns(),
|
||||||
|
...table.getRightVisibleLeafColumns(),
|
||||||
|
];
|
||||||
|
const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150;
|
||||||
|
|
||||||
|
return {
|
||||||
|
overscan: 10,
|
||||||
|
estimateSize: getColumnSize,
|
||||||
|
measureElement: (element) => {
|
||||||
|
if (!element) return 150;
|
||||||
|
return getColumnSize(Number(element.getAttribute('data-index')));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasVspDropdown = (columns) =>
|
||||||
|
columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns)));
|
||||||
|
|
||||||
|
const tableContentStyle = {
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
height: '100%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadingOverlayStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'rgba(255, 255, 255, 0.6)',
|
||||||
|
zIndex: 2,
|
||||||
|
};
|
||||||
|
|
||||||
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||||
const { data, setData, columnsCurStage, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
const { user } = useAuth();
|
||||||
|
const userRoleId = user?.role_id;
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
columnsCurStage,
|
||||||
|
isLoading: isTableLoading,
|
||||||
|
editingCells: dataEditingCells,
|
||||||
|
} = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||||
const [selectedColumnId, setSelectedColumnId] = useState();
|
const [selectedColumnId, setSelectedColumnId] = useState();
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const [isLoadingData, setIsLoadingData] = useState(false);
|
const rowSelectionRef = useRef(rowSelection);
|
||||||
const [isPending, startTransition] = useTransition();
|
rowSelectionRef.current = rowSelection;
|
||||||
|
const [, startTransition] = useTransition();
|
||||||
const [editingCell, setEditingCell] = useState(null);
|
const [editingCell, setEditingCell] = useState(null);
|
||||||
const [columnsConfig, setColumnsConfig] = useState(null);
|
const [columnsConfig, setColumnsConfig] = useState(null);
|
||||||
const { errors: validationErrors, isCellInvalid } = useValidationRules(
|
const { errors: validationErrors, isCellInvalid } = useValidationRules(data, columnsConfig?.columns, formType, sheetName);
|
||||||
data,
|
|
||||||
columnsConfig?.columns,
|
|
||||||
formType,
|
|
||||||
sheetName,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
const [showOnlyDepth2, setShowOnlyDepth2] = useState(false);
|
const [showOnlyDepth2, setShowOnlyDepth2] = useState(false);
|
||||||
|
|
||||||
const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
|
const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
|
||||||
const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
|
const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
|
||||||
const [isOpenModalCreateProgram, setIsOpenModalCreateProgram] = useState(false);
|
const [createModalType, setCreateModalType] = useState(null);
|
||||||
const [isOpenModalCreateProject, setIsOpenModalCreateProject] = useState(false);
|
|
||||||
const [isProgramCreate, setIsProgramCreate] = useState(false)
|
|
||||||
|
|
||||||
const [isFormNoCanAddRow, setIsFormNoCanAddRow] = useState(() => {
|
const isFormNoCanAddRow = useMemo(() => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false, [formType, sheetName]);
|
||||||
return (FormsNoCanAddRow[formType].includes(sheetName));
|
|
||||||
})
|
|
||||||
|
|
||||||
const isVspAdditionRow = useMemo(() => {
|
const isVspAdditionRow = useMemo(() => {
|
||||||
if (!formType || !sheetName) return false;
|
if (!formType || !sheetName) return false;
|
||||||
@ -78,21 +118,19 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
return additionExpenseRowTable[formType].includes(sheetName);
|
return additionExpenseRowTable[formType].includes(sheetName);
|
||||||
}, [formType, sheetName]);
|
}, [formType, sheetName]);
|
||||||
|
|
||||||
const handleGlobalFilterChange = useCallback(
|
const handleGlobalFilterChange = useMemo(
|
||||||
debounce((value) => {
|
() =>
|
||||||
startTransition(() => {
|
debounce((value) => {
|
||||||
setGlobalFilter(value);
|
startTransition(() => {
|
||||||
});
|
setGlobalFilter(value);
|
||||||
}, 300),
|
});
|
||||||
[]
|
}, 300),
|
||||||
|
[startTransition],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => () => handleGlobalFilterChange.clear(), [handleGlobalFilterChange]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnected,
|
|
||||||
isAuthenticated,
|
|
||||||
subscribeToErrors,
|
|
||||||
subscribeToAuthSuccess,
|
|
||||||
error: wsError,
|
|
||||||
updateCell: contextUpdateCell,
|
updateCell: contextUpdateCell,
|
||||||
addRow: contextAddRow,
|
addRow: contextAddRow,
|
||||||
deleteRow: contextDeleteRow,
|
deleteRow: contextDeleteRow,
|
||||||
@ -100,7 +138,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
addProgram: contextAddProgram,
|
addProgram: contextAddProgram,
|
||||||
addProject: contextAddProject,
|
addProject: contextAddProject,
|
||||||
setVspOptions,
|
setVspOptions,
|
||||||
} = useRealtime();
|
} = useRealtimeActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
columnSizing,
|
columnSizing,
|
||||||
@ -112,73 +150,76 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
handleUnpinColumn,
|
handleUnpinColumn,
|
||||||
handleSetColumnWidth,
|
handleSetColumnWidth,
|
||||||
handleToggleColumnVisibility,
|
handleToggleColumnVisibility,
|
||||||
|
handleSetColumnsVisibility,
|
||||||
} = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`);
|
} = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`);
|
||||||
|
|
||||||
const { headerPortalRef, containerRef } = useHeaderPortal();
|
const { headerPortalRef, containerRef } = useHeaderPortal();
|
||||||
const rowVirtualizerRef = useRef(null);
|
const rowVirtualizerRef = useRef(null);
|
||||||
|
const columnVirtualizerRef = useRef(null);
|
||||||
|
|
||||||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } =
|
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale();
|
||||||
useTableScale();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rowVirtualizerRef.current?.measure?.();
|
rowVirtualizerRef.current?.measure?.();
|
||||||
|
columnVirtualizerRef.current?.measure?.();
|
||||||
}, [sizeMult]);
|
}, [sizeMult]);
|
||||||
|
|
||||||
const configCache = useRef(new Map());
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formType || !sheetName) return;
|
if (!formType || !sheetName) return;
|
||||||
|
let isCancelled = false;
|
||||||
const cacheKey = `${formType}_${sheetName}`;
|
const cacheKey = `${formType}_${sheetName}`;
|
||||||
|
|
||||||
if (configCache.current.has(cacheKey)) {
|
if (CONFIG_CACHE.has(cacheKey)) {
|
||||||
setColumnsConfig(configCache.current.get(cacheKey));
|
setColumnsConfig(CONFIG_CACHE.get(cacheKey));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
||||||
configCache.current.set(cacheKey, config.config);
|
CONFIG_CACHE.set(cacheKey, config.config);
|
||||||
setColumnsConfig(config.config);
|
if (!isCancelled) setColumnsConfig(config.config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
||||||
setColumnsConfig({ columns: [], colors: {} });
|
if (!isCancelled) setColumnsConfig({ columns: [], colors: {} });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
}, [formType, sheetName]);
|
}, [formType, sheetName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formId || !columnsConfig?.columns) return;
|
if (!formId || !columnsConfig?.columns) return;
|
||||||
|
|
||||||
const hasVspDropdown = (cols) => {
|
|
||||||
for (const col of cols) {
|
|
||||||
if (col.editType === 'vsp_dropdown') return true;
|
|
||||||
if (col.columns && hasVspDropdown(col.columns)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!hasVspDropdown(columnsConfig.columns)) return;
|
if (!hasVspDropdown(columnsConfig.columns)) return;
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
DictVspApi.getDropdownVsp({ form_id: formId }).then(data => {
|
DictVspApi.getDropdownVsp({ form_id: formId }).then((data) => {
|
||||||
if (data.success) {
|
if (!isCancelled && data.success) {
|
||||||
setVspOptions(data.result);
|
setVspOptions(data.result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
}, [formId, columnsConfig, setVspOptions]);
|
}, [formId, columnsConfig, setVspOptions]);
|
||||||
|
|
||||||
|
const handleUpdateCell = useCallback(
|
||||||
const handleUpdateCell = useCallback(async (row, column, value) => {
|
(row, column, value) => {
|
||||||
return await contextUpdateCell(row, column, value);
|
return contextUpdateCell(row, column, value);
|
||||||
}, [contextUpdateCell]);
|
},
|
||||||
|
[contextUpdateCell],
|
||||||
|
);
|
||||||
|
|
||||||
const handleClickRowCell = useCallback((rowId) => {
|
const handleClickRowCell = useCallback((rowId) => {
|
||||||
setRowSelection((prev) => ({
|
setRowSelection((prev) => ({
|
||||||
[rowId]: !prev[rowId],
|
[rowId]: !prev[rowId],
|
||||||
}));
|
}));
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleColumnSelect = useCallback((columnId) => {
|
const handleColumnSelect = useCallback((columnId) => {
|
||||||
setSelectedColumnId((prev) => {
|
setSelectedColumnId((prev) => {
|
||||||
@ -215,7 +256,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
EditCell,
|
EditCell,
|
||||||
columnsConfig,
|
columnsConfig,
|
||||||
columnsCurStage,
|
columnsCurStage,
|
||||||
onCellUpdate: handleUpdateCell,
|
userRoleId,
|
||||||
onCellNumberClick: handleClickRowCell,
|
onCellNumberClick: handleClickRowCell,
|
||||||
onCellUpdateError: handleCellUpdateError,
|
onCellUpdateError: handleCellUpdateError,
|
||||||
isCellInvalid,
|
isCellInvalid,
|
||||||
@ -224,33 +265,40 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
console.error('Error creating columns:', error);
|
console.error('Error creating columns:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [
|
}, [columnsConfig, columnsCurStage, userRoleId, handleCellUpdateError, handleClickRowCell, handleUpdateCell, isCellInvalid]);
|
||||||
columnsConfig,
|
|
||||||
columnsCurStage,
|
|
||||||
handleCellUpdateError,
|
|
||||||
handleClickRowCell,
|
|
||||||
handleUpdateCell,
|
|
||||||
isCellInvalid,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const selectedColumnIdRef = useRef(selectedColumnId);
|
// MRT запоминает индексы pinned-колонок до завершения асинхронной
|
||||||
|
// загрузки конфигурации. Новая ссылка на columnPinning заставляет
|
||||||
|
// виртуализатор пересчитать эти индексы уже с полным набором колонок.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!columnsConfig?.columns?.length) return;
|
||||||
|
|
||||||
|
setColumnPinning((currentPinning) => ({
|
||||||
|
left: [...(currentPinning.left || [])],
|
||||||
|
right: [...(currentPinning.right || [])],
|
||||||
|
}));
|
||||||
|
}, [columnsConfig, setColumnPinning]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedColumnIdRef.current = selectedColumnId;
|
if (!columns.length) return;
|
||||||
}, [selectedColumnId]);
|
|
||||||
|
|
||||||
const tableMeta = useMemo(() => ({
|
const animationFrameId = requestAnimationFrame(() => {
|
||||||
updateCell: handleUpdateCell,
|
columnVirtualizerRef.current?.measure?.();
|
||||||
getSelectedColumnId: () => selectedColumnIdRef.current,
|
});
|
||||||
}), [handleUpdateCell]);
|
|
||||||
|
|
||||||
const ROW_VIRTUALIZER_OPTIONS = {
|
return () => cancelAnimationFrame(animationFrameId);
|
||||||
overscan: 5,
|
}, [columns, columnPinning, columnSizing, columnVisibility]);
|
||||||
scrollPaddingStart: 0,
|
|
||||||
scrollPaddingEnd: 0,
|
const selectedColumnIdRef = useRef(selectedColumnId);
|
||||||
estimateSize: () => TABLE_ROW_HEIGHT,
|
selectedColumnIdRef.current = selectedColumnId;
|
||||||
measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT,
|
|
||||||
};
|
const tableMeta = useMemo(
|
||||||
|
() => ({
|
||||||
|
updateCell: handleUpdateCell,
|
||||||
|
getSelectedColumnId: () => selectedColumnIdRef.current,
|
||||||
|
}),
|
||||||
|
[handleUpdateCell],
|
||||||
|
);
|
||||||
|
|
||||||
const createTableConfig = ({
|
const createTableConfig = ({
|
||||||
columns,
|
columns,
|
||||||
@ -276,27 +324,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
enableRowVirtualization: true,
|
enableRowVirtualization: true,
|
||||||
enableColumnVirtualization: true,
|
enableColumnVirtualization: true,
|
||||||
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
rowVirtualizerInstanceRef: rowVirtualizerRef,
|
||||||
|
columnVirtualizerInstanceRef: columnVirtualizerRef,
|
||||||
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
|
rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS,
|
||||||
onEditingCellChange: (cell) => {
|
onEditingCellChange: (cell) => {
|
||||||
if (cell && !editingCell) {
|
if (cell && !editingCell) {
|
||||||
contextStartEditing?.(cell.row, cell.column);
|
contextStartEditing?.(cell.row, cell.column);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
columnVirtualizerOptions: ({ table }) => ({
|
columnVirtualizerOptions: getColumnVirtualizerOptions,
|
||||||
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,
|
enableRowSelection: true,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
onColumnSizingChange: setColumnSizing,
|
onColumnSizingChange: setColumnSizing,
|
||||||
@ -311,7 +346,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
showColumnFilters,
|
showColumnFilters,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
editingCell,
|
editingCell,
|
||||||
expanded
|
expanded,
|
||||||
},
|
},
|
||||||
initialState: {
|
initialState: {
|
||||||
expanded: true,
|
expanded: true,
|
||||||
@ -333,7 +368,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
minHeight: '1px',
|
minHeight: '1px',
|
||||||
maxHeight: '1px',
|
maxHeight: '1px',
|
||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
muiTablePaperProps: getTablePaperStyles(),
|
muiTablePaperProps: getTablePaperStyles(),
|
||||||
@ -352,68 +387,74 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const tableConfig = useMemo(() => createTableConfig({
|
const tableConfig = useMemo(
|
||||||
columns,
|
() =>
|
||||||
data,
|
createTableConfig({
|
||||||
columnSizing,
|
columns,
|
||||||
columnPinning,
|
data,
|
||||||
columnVisibility,
|
columnSizing,
|
||||||
expanded,
|
columnPinning,
|
||||||
globalFilter,
|
columnVisibility,
|
||||||
showColumnFilters,
|
expanded,
|
||||||
rowSelection,
|
globalFilter,
|
||||||
containerRef,
|
showColumnFilters,
|
||||||
tableMeta,
|
rowSelection,
|
||||||
setColumnSizing,
|
containerRef,
|
||||||
setColumnPinning,
|
tableMeta,
|
||||||
setExpanded,
|
setColumnSizing,
|
||||||
editingCell,
|
setColumnPinning,
|
||||||
contextStartEditing,
|
setExpanded,
|
||||||
}), [
|
editingCell,
|
||||||
columns,
|
contextStartEditing,
|
||||||
data,
|
}),
|
||||||
columnSizing,
|
[
|
||||||
columnPinning,
|
columns,
|
||||||
columnVisibility,
|
data,
|
||||||
globalFilter,
|
columnSizing,
|
||||||
showColumnFilters,
|
columnPinning,
|
||||||
rowSelection,
|
columnVisibility,
|
||||||
containerRef,
|
globalFilter,
|
||||||
tableMeta,
|
showColumnFilters,
|
||||||
setColumnSizing,
|
rowSelection,
|
||||||
setColumnPinning,
|
containerRef,
|
||||||
editingCell,
|
tableMeta,
|
||||||
expanded,
|
setColumnSizing,
|
||||||
]);
|
setColumnPinning,
|
||||||
|
editingCell,
|
||||||
|
expanded,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const table = useMaterialReactTable(tableConfig);
|
const table = useMaterialReactTable(tableConfig);
|
||||||
|
const isLoadingData = columns.length > 0;
|
||||||
|
|
||||||
const handleNavigateToColumn = useCallback((columnId) => {
|
const handleNavigateToColumn = useCallback(
|
||||||
if (!columnId) return;
|
(columnId) => {
|
||||||
|
if (!columnId) return;
|
||||||
|
|
||||||
setSelectedColumnId(columnId);
|
setSelectedColumnId(columnId);
|
||||||
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const pinnedIds = new Set(columnPinning?.left || []);
|
const pinnedIds = new Set(columnPinning?.left || []);
|
||||||
if (pinnedIds.has(columnId)) return;
|
if (pinnedIds.has(columnId)) return;
|
||||||
|
|
||||||
const centerColumns = table
|
const centerColumns = table.getVisibleLeafColumns().filter((column) => !pinnedIds.has(column.id));
|
||||||
.getVisibleLeafColumns()
|
|
||||||
.filter((column) => !pinnedIds.has(column.id));
|
|
||||||
|
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
for (const column of centerColumns) {
|
for (const column of centerColumns) {
|
||||||
if (column.id === columnId) break;
|
if (column.id === columnId) break;
|
||||||
offset += column.getSize();
|
offset += column.getSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
container.scrollTo({
|
container.scrollTo({
|
||||||
left: Math.max(0, offset - 40),
|
left: Math.max(0, offset - 40),
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}, [columnPinning, containerRef, table]);
|
},
|
||||||
|
[columnPinning, containerRef, table],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataEditingCells?.line_id) {
|
if (!dataEditingCells?.line_id) {
|
||||||
@ -422,99 +463,90 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
}
|
}
|
||||||
if (editingCell) return;
|
if (editingCell) return;
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
const animationFrameId = requestAnimationFrame(() => {
|
||||||
try {
|
try {
|
||||||
const row = table.getRow(dataEditingCells.line_id);
|
const row = table.getRow(dataEditingCells.line_id);
|
||||||
const cell = row?.getVisibleCells().find(
|
const cell = row?.getVisibleCells().find((c) => c.column.id === dataEditingCells.column);
|
||||||
c => c.column.id === dataEditingCells.column
|
|
||||||
);
|
|
||||||
if (cell) setEditingCell(cell);
|
if (cell) setEditingCell(cell);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => cancelAnimationFrame(animationFrameId);
|
||||||
}, [dataEditingCells, editingCell, table]);
|
}, [dataEditingCells, editingCell, table]);
|
||||||
|
|
||||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
const getSelectedRow = useCallback(() => table.getRowModel().rows.find((row) => rowSelectionRef.current[row.id]), [table]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFirstLoad && table && data && columns.length !== 0) {
|
|
||||||
setIsFirstLoad(false);
|
|
||||||
setIsLoadingData(true);
|
|
||||||
}
|
|
||||||
}, [data, columns, table, isFirstLoad]);
|
|
||||||
|
|
||||||
const handleAddRow = useCallback(() => {
|
const handleAddRow = useCallback(() => {
|
||||||
const allRows = table.getRowModel().rows;
|
const row = getSelectedRow();
|
||||||
|
if (!row) {
|
||||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
|
||||||
if (selectedRows.length === 0) {
|
|
||||||
toast.error('Выделите строку для вставки');
|
toast.error('Выделите строку для вставки');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = selectedRows[0];
|
contextAddRow({ expense_item_id: row.original.data.header.expense_item_id });
|
||||||
const expense_item_id = row.original.data.header.expense_item_id;
|
}, [contextAddRow, getSelectedRow]);
|
||||||
contextAddRow({ expense_item_id: expense_item_id })
|
|
||||||
}, [rowSelection, table])
|
|
||||||
|
|
||||||
const handleAddVspRow = useCallback((vsp_id) => {
|
const handleAddVspRow = useCallback(
|
||||||
contextAddRow({ vsp_id: vsp_id });
|
(vsp_id) => {
|
||||||
}, []);
|
contextAddRow({ vsp_id });
|
||||||
|
},
|
||||||
|
[contextAddRow],
|
||||||
|
);
|
||||||
|
|
||||||
const handleAddExpenseItemRow = useCallback((expense_item) => {
|
const handleAddExpenseItemRow = useCallback(
|
||||||
const allRows = table.getRowModel().rows;
|
(expense_item) => {
|
||||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
const row = getSelectedRow();
|
||||||
const row = selectedRows[0];
|
if (!row) return;
|
||||||
contextAddRow({ expense_item_id: expense_item.id, project_id: row.original.data.header.project_id });
|
contextAddRow({ expense_item_id: expense_item.id, project_id: row.original.data.header.project_id });
|
||||||
}, [rowSelection]);
|
},
|
||||||
|
[contextAddRow, getSelectedRow],
|
||||||
|
);
|
||||||
|
|
||||||
const handleAddProgramRow = useCallback((name) => {
|
const handleAddProgramRow = useCallback(
|
||||||
contextAddProgram({ name: name });
|
(name) => {
|
||||||
}, []);
|
contextAddProgram({ name });
|
||||||
|
},
|
||||||
|
[contextAddProgram],
|
||||||
|
);
|
||||||
|
|
||||||
const handleAddProjectRow = useCallback((name) => {
|
const handleAddProjectRow = useCallback(
|
||||||
const allRows = table.getRowModel().rows;
|
(name) => {
|
||||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
const row = getSelectedRow();
|
||||||
const row = selectedRows[0];
|
if (!row) return;
|
||||||
contextAddProject({ name: name, program_id: row.original.data.header.program_id });
|
contextAddProject({ name, program_id: row.original.data.header.program_id });
|
||||||
}, [rowSelection]);
|
},
|
||||||
|
[contextAddProject, getSelectedRow],
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpenModalSelectVsp = useCallback(() => {
|
const handleOpenModalSelectVsp = useCallback(() => {
|
||||||
setIsOpenModalSelectVsp(true);
|
setIsOpenModalSelectVsp(true);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleOpenModalSelectExpenseItem = useCallback(() => {
|
const handleOpenModalSelectExpenseItem = useCallback(() => {
|
||||||
const allRows = table.getRowModel().rows;
|
const row = getSelectedRow();
|
||||||
|
if (!row) {
|
||||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
|
||||||
if (selectedRows.length === 0) {
|
|
||||||
toast.error('Выделите строку для вставки');
|
toast.error('Выделите строку для вставки');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = selectedRows[0];
|
|
||||||
if (row.original.row_type !== 'ITEM') {
|
if (row.original.row_type !== 'ITEM') {
|
||||||
toast.error('Выделите строку проекта');
|
toast.error('Выделите строку проекта');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsOpenModalSelectExpenseItem(true);
|
setIsOpenModalSelectExpenseItem(true);
|
||||||
}, [rowSelection])
|
}, [getSelectedRow]);
|
||||||
|
|
||||||
const handleDeleteRow = useCallback(() => {
|
const handleDeleteRow = useCallback(() => {
|
||||||
const allRows = table.getRowModel().rows;
|
const row = getSelectedRow();
|
||||||
|
if (!row) {
|
||||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
|
||||||
if (selectedRows.length === 0) {
|
|
||||||
toast.error('Выделите строку для удаления');
|
toast.error('Выделите строку для удаления');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = selectedRows[0];
|
contextDeleteRow(getRowId(row));
|
||||||
const rowId = getRowId(row);
|
|
||||||
contextDeleteRow(rowId);
|
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
}, [rowSelection, table])
|
}, [contextDeleteRow, getSelectedRow]);
|
||||||
|
|
||||||
const addRow = useCallback(() => {
|
const addRow = useCallback(() => {
|
||||||
|
|
||||||
if (isVspAdditionRow) {
|
if (isVspAdditionRow) {
|
||||||
return handleOpenModalSelectVsp();
|
return handleOpenModalSelectVsp();
|
||||||
}
|
}
|
||||||
@ -525,45 +557,39 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
}, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]);
|
}, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]);
|
||||||
|
|
||||||
const addProgram = useCallback(() => {
|
const addProgram = useCallback(() => {
|
||||||
setIsProgramCreate(true);
|
setCreateModalType('program');
|
||||||
setIsOpenModalCreateProgram(true);
|
}, []);
|
||||||
}, [])
|
|
||||||
|
|
||||||
const addProject = useCallback(() => {
|
const addProject = useCallback(() => {
|
||||||
setIsProgramCreate(false);
|
const row = getSelectedRow();
|
||||||
const allRows = table.getRowModel().rows;
|
if (!row) {
|
||||||
|
|
||||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
|
||||||
if (selectedRows.length === 0) {
|
|
||||||
toast.error('Выделите строку для вставки');
|
toast.error('Выделите строку для вставки');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = selectedRows[0];
|
|
||||||
if (row.original.row_type !== 'GROUP') {
|
if (row.original.row_type !== 'GROUP') {
|
||||||
toast.error('Выделите строку программы');
|
toast.error('Выделите строку программы');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsOpenModalCreateProgram(true);
|
setCreateModalType('project');
|
||||||
}, [setIsOpenModalCreateProgram, rowSelection])
|
}, [getSelectedRow]);
|
||||||
|
|
||||||
|
const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []);
|
||||||
|
const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []);
|
||||||
|
const closeCreateModal = useCallback(() => setCreateModalType(null), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
setData([]);
|
rowVirtualizerRef.current = null;
|
||||||
setRowSelection({});
|
columnVirtualizerRef.current = null;
|
||||||
setEditingCell(null);
|
|
||||||
if (rowVirtualizerRef.current) {
|
|
||||||
rowVirtualizerRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleToggleDepthVisibility = useCallback(() => {
|
const handleToggleDepthVisibility = useCallback(() => {
|
||||||
setShowOnlyDepth2(prev => {
|
setShowOnlyDepth2((prev) => {
|
||||||
const newState = !prev;
|
const newState = !prev;
|
||||||
|
|
||||||
if (newState) {
|
if (newState) {
|
||||||
const allRows = table.getRowModel().flatRows;
|
const allRows = table.getRowModel().flatRows;
|
||||||
console.log(allRows);
|
|
||||||
const newExpanded = {};
|
const newExpanded = {};
|
||||||
|
|
||||||
for (const row of allRows) {
|
for (const row of allRows) {
|
||||||
@ -588,22 +614,22 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
<SettingsPanel
|
<SettingsPanel
|
||||||
selectedColumnId={selectedColumnId}
|
selectedColumnId={selectedColumnId}
|
||||||
columnPinning={columnPinning}
|
columnPinning={columnPinning}
|
||||||
table={table}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onPinColumn={handlePinColumn}
|
onPinColumn={handlePinColumn}
|
||||||
onUnpinColumn={handleUnpinColumn}
|
onUnpinColumn={handleUnpinColumn}
|
||||||
onToggleColumnVisibility={handleToggleColumnVisibility}
|
onToggleColumnVisibility={handleToggleColumnVisibility}
|
||||||
|
onSetColumnsVisibility={handleSetColumnsVisibility}
|
||||||
columnVisibility={columnVisibility}
|
columnVisibility={columnVisibility}
|
||||||
columnsCurStage={columnsCurStage}
|
columnsCurStage={columnsCurStage}
|
||||||
onChangeSizeMult={setSizeMult}
|
onChangeSizeMult={setSizeMult}
|
||||||
onGlobalFilterChange={handleGlobalFilterChange}
|
onGlobalFilterChange={handleGlobalFilterChange}
|
||||||
sizeMult={sizeMult}
|
sizeMult={sizeMult}
|
||||||
onChangeShowColumnFilters={setShowColumnFilters}
|
onChangeShowColumnFilters={setShowColumnFilters}
|
||||||
|
showColumnFilters={showColumnFilters}
|
||||||
onAddRow={addRow}
|
onAddRow={addRow}
|
||||||
onAddProgram={addProgram}
|
onAddProgram={addProgram}
|
||||||
onAddProject={addProject}
|
onAddProject={addProject}
|
||||||
onDeleteRow={handleDeleteRow}
|
onDeleteRow={handleDeleteRow}
|
||||||
isLoadingData={isLoadingData}
|
|
||||||
formId={formId}
|
formId={formId}
|
||||||
sheetName={sheetName}
|
sheetName={sheetName}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
@ -618,15 +644,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
|
|
||||||
<div style={tableWrapperStyle}>
|
<div style={tableWrapperStyle}>
|
||||||
<div style={tableScaleStyle}>
|
<div style={tableScaleStyle}>
|
||||||
<div
|
<div style={tableContentStyle}>
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 0,
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialReactTable table={table} />
|
<MaterialReactTable table={table} />
|
||||||
|
|
||||||
<ColumnSelectionOverlay
|
<ColumnSelectionOverlay
|
||||||
@ -639,17 +657,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{isTableLoading && (
|
{isTableLoading && (
|
||||||
<div
|
<div style={loadingOverlayStyle}>
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
inset: 0,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
background: 'rgba(255, 255, 255, 0.6)',
|
|
||||||
zIndex: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -658,7 +666,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
createPortal(
|
createPortal(
|
||||||
<TableHead
|
<TableHead
|
||||||
table={table}
|
table={table}
|
||||||
style={{ zIndex: (theme) => theme.zIndex.modal - 1 }}
|
columns={columns}
|
||||||
|
columnPinning={columnPinning}
|
||||||
|
columnSizing={columnSizing}
|
||||||
|
columnVisibility={columnVisibility}
|
||||||
|
columnFilters={table.getState().columnFilters}
|
||||||
|
showColumnFilters={showColumnFilters}
|
||||||
selectedColumnId={selectedColumnId}
|
selectedColumnId={selectedColumnId}
|
||||||
onColumnSelect={handleColumnSelect}
|
onColumnSelect={handleColumnSelect}
|
||||||
onChangeWidth={handleSetColumnWidth}
|
onChangeWidth={handleSetColumnWidth}
|
||||||
@ -671,24 +684,24 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
</div>
|
</div>
|
||||||
<SelectVspModal
|
<SelectVspModal
|
||||||
isOpen={isOpenModalSelectVsp}
|
isOpen={isOpenModalSelectVsp}
|
||||||
onClose={() => setIsOpenModalSelectVsp(false)}
|
onClose={closeSelectVspModal}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSelect={handleAddVspRow}
|
onSelect={handleAddVspRow}
|
||||||
title="Выбор ВСП"
|
title='Выбор ВСП'
|
||||||
/>
|
/>
|
||||||
<SelectExpenseItemModal
|
<SelectExpenseItemModal
|
||||||
isOpen={isOpenModalSelectExpenseItem}
|
isOpen={isOpenModalSelectExpenseItem}
|
||||||
onClose={() => setIsOpenModalSelectExpenseItem(false)}
|
onClose={closeSelectExpenseItemModal}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSelect={handleAddExpenseItemRow}
|
onSelect={handleAddExpenseItemRow}
|
||||||
title="Добавление строки"
|
title='Добавление строки'
|
||||||
sheet={sheetName}
|
sheet={sheetName}
|
||||||
/>
|
/>
|
||||||
<CreateProgramModal
|
<CreateProgramModal
|
||||||
isOpen={isOpenModalCreateProgram}
|
isOpen={createModalType !== null}
|
||||||
onClose={() => setIsOpenModalCreateProgram(false)}
|
onClose={closeCreateModal}
|
||||||
title={isProgramCreate ? "Создание программы" : "Создание проекта"}
|
title={createModalType === 'program' ? 'Создание программы' : 'Создание проекта'}
|
||||||
onCreate={isProgramCreate ? handleAddProgramRow : handleAddProjectRow}
|
onCreate={createModalType === 'program' ? handleAddProgramRow : handleAddProjectRow}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { Divider, IconButton, Stack, Tooltip } from '@mui/material';
|
import { Divider, IconButton, Stack, Tooltip } from '@mui/material';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { FORM_TYPE_OPTIONS } from '../../../constants/constants';
|
import { FORM_TYPE_OPTIONS } from '../../../constants/constants';
|
||||||
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
import { exportSheet, exportSheetProject } from '../../../utils/exportFile';
|
||||||
|
import { stageColumnsHiddenFromPicker } from '../../Stages/constants';
|
||||||
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
|
||||||
import CollapsibleTree from '../../common/CollapsibleTree';
|
import CollapsibleTree from '../../common/CollapsibleTree';
|
||||||
import SearchComponent from '../../common/SearchComponent';
|
import SearchComponent from '../../common/SearchComponent';
|
||||||
@ -19,37 +20,39 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
VisibleExpenseItems,
|
VisibleExpenseItems,
|
||||||
} from '../../common/icons/icons';
|
} from '../../common/icons/icons';
|
||||||
|
import { collectLeafColumns } from '../utils/columnUtils';
|
||||||
import { GroupByObject } from './GroupByObject/GroupByObject';
|
import { GroupByObject } from './GroupByObject/GroupByObject';
|
||||||
import { Panel } from './SettingPanel.style';
|
import { Panel } from './SettingPanel.style';
|
||||||
import ValidationErrorsAlert from './ValidationErrorsAlert';
|
import ValidationErrorsAlert from './ValidationErrorsAlert';
|
||||||
import ZoomSlider from './ZoomSlider';
|
import ZoomSlider from './ZoomSlider';
|
||||||
import { collectLeafColumns } from '../utils/columnUtils';
|
|
||||||
import { stageColumnsHiddenFromPicker } from '../../Stages/constants';
|
|
||||||
|
|
||||||
const controlSx = {
|
const controlSx = {
|
||||||
minHeight: '2.5rem',
|
minHeight: '2.5rem',
|
||||||
flex: '0 0 2.5rem',
|
flex: '0 0 2.5rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchSx = { height: '2.5rem' };
|
||||||
|
const exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' };
|
||||||
|
|
||||||
const SettingPanel = ({
|
const SettingPanel = ({
|
||||||
selectedColumnId,
|
selectedColumnId,
|
||||||
columnPinning,
|
columnPinning,
|
||||||
table,
|
|
||||||
columns,
|
columns,
|
||||||
onPinColumn,
|
onPinColumn,
|
||||||
onUnpinColumn,
|
onUnpinColumn,
|
||||||
onToggleColumnVisibility,
|
onToggleColumnVisibility,
|
||||||
|
onSetColumnsVisibility,
|
||||||
columnVisibility,
|
columnVisibility,
|
||||||
columnsCurStage,
|
columnsCurStage,
|
||||||
onChangeSizeMult,
|
onChangeSizeMult,
|
||||||
onGlobalFilterChange,
|
onGlobalFilterChange,
|
||||||
sizeMult,
|
sizeMult,
|
||||||
onChangeShowColumnFilters,
|
onChangeShowColumnFilters,
|
||||||
|
showColumnFilters,
|
||||||
onAddRow,
|
onAddRow,
|
||||||
onAddProgram,
|
onAddProgram,
|
||||||
onAddProject,
|
onAddProject,
|
||||||
onDeleteRow,
|
onDeleteRow,
|
||||||
isLoadingData,
|
|
||||||
formId,
|
formId,
|
||||||
sheetName,
|
sheetName,
|
||||||
direction,
|
direction,
|
||||||
@ -61,54 +64,39 @@ const SettingPanel = ({
|
|||||||
showOnlyDepth2,
|
showOnlyDepth2,
|
||||||
onToggleDepthVisibility,
|
onToggleDepthVisibility,
|
||||||
}) => {
|
}) => {
|
||||||
const [isPinned, setIsPinned] = useState(false);
|
|
||||||
const [isExporting, setIsExporting] = useState(false);
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
const [selectedColumnIds, setSelectedColumnIds] = useState();
|
const isPinned = useMemo(
|
||||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
() => Boolean(selectedColumnId && columnPinning?.left?.includes(selectedColumnId)),
|
||||||
|
[columnPinning, selectedColumnId],
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedColumnId) {
|
|
||||||
setIsPinned(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pinningColumn = columnPinning?.left || table.getState().columnPinning.left || [];
|
|
||||||
const pinned = pinningColumn.includes(selectedColumnId);
|
|
||||||
setIsPinned(pinned);
|
|
||||||
}, [selectedColumnId, columnPinning, table]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedColumnIds(Object.keys(columnVisibility).filter((key) => !columnVisibility[key]));
|
|
||||||
}, [columnVisibility]);
|
|
||||||
|
|
||||||
//TO DO: сделать
|
|
||||||
const { addColorForCells } = {};
|
|
||||||
|
|
||||||
const handleChangeShowColumnFilters = () => {
|
|
||||||
const show = table.getState().showColumnFilters;
|
|
||||||
setShowColumnFilters(!show);
|
|
||||||
onChangeShowColumnFilters(!show);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
|
||||||
if (isExporting) return;
|
|
||||||
if (formType === 'PROJECT') {
|
|
||||||
if (!formId || !sheetName || !year) return;
|
|
||||||
setIsExporting(true);
|
|
||||||
await exportSheetProject(formId, sheetName, year);
|
|
||||||
setIsExporting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formId || !sheetName) return;
|
|
||||||
setIsExporting(true);
|
|
||||||
await exportSheet(formId, sheetName, direction);
|
|
||||||
setIsExporting(false);
|
|
||||||
};
|
|
||||||
const leafColumns = useMemo(() => collectLeafColumns({ columns }), [columns]);
|
|
||||||
const stageColumnIds = useMemo(
|
|
||||||
() => new Set([...stageColumnsHiddenFromPicker, ...(columnsCurStage || [])]),
|
|
||||||
[columnsCurStage],
|
|
||||||
);
|
);
|
||||||
|
const selectedColumnIds = useMemo(() => Object.keys(columnVisibility).filter((key) => !columnVisibility[key]), [columnVisibility]);
|
||||||
|
|
||||||
|
const handleChangeShowColumnFilters = useCallback(() => {
|
||||||
|
onChangeShowColumnFilters(!showColumnFilters);
|
||||||
|
}, [onChangeShowColumnFilters, showColumnFilters]);
|
||||||
|
|
||||||
|
const handleExport = useCallback(async () => {
|
||||||
|
if (isExporting) return;
|
||||||
|
if (!formId || !sheetName || (formType === 'PROJECT' && !year)) return;
|
||||||
|
|
||||||
|
setIsExporting(true);
|
||||||
|
try {
|
||||||
|
if (formType === 'PROJECT') {
|
||||||
|
await exportSheetProject(formId, sheetName, year);
|
||||||
|
} else {
|
||||||
|
await exportSheet(formId, sheetName, direction);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsExporting(false);
|
||||||
|
}
|
||||||
|
}, [direction, formId, formType, isExporting, sheetName, year]);
|
||||||
|
|
||||||
|
const handleTogglePin = useCallback(() => {
|
||||||
|
if (!selectedColumnId) return;
|
||||||
|
isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId);
|
||||||
|
}, [isPinned, onPinColumn, onUnpinColumn, selectedColumnId]);
|
||||||
|
const leafColumns = useMemo(() => collectLeafColumns({ columns }), [columns]);
|
||||||
|
const stageColumnIds = useMemo(() => new Set([...stageColumnsHiddenFromPicker, ...(columnsCurStage || [])]), [columnsCurStage]);
|
||||||
const showOnlyCurrentStageColumns = useMemo(() => {
|
const showOnlyCurrentStageColumns = useMemo(() => {
|
||||||
if (!columnsCurStage?.length || !leafColumns.length) return false;
|
if (!columnsCurStage?.length || !leafColumns.length) return false;
|
||||||
return leafColumns.every((column) =>
|
return leafColumns.every((column) =>
|
||||||
@ -121,10 +109,20 @@ const SettingPanel = ({
|
|||||||
console.warn('Нет столбцов для текущего этапа');
|
console.warn('Нет столбцов для текущего этапа');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const visibilityByColumnId = {};
|
||||||
for (const column of leafColumns) {
|
for (const column of leafColumns) {
|
||||||
onToggleColumnVisibility(column, showOnlyCurrentStageColumns || stageColumnIds.has(column.id));
|
visibilityByColumnId[column.id] = showOnlyCurrentStageColumns || stageColumnIds.has(column.id);
|
||||||
}
|
}
|
||||||
}, [columnsCurStage, leafColumns, onToggleColumnVisibility, showOnlyCurrentStageColumns, stageColumnIds]);
|
onSetColumnsVisibility(visibilityByColumnId);
|
||||||
|
}, [columnsCurStage, leafColumns, onSetColumnsVisibility, showOnlyCurrentStageColumns, stageColumnIds]);
|
||||||
|
|
||||||
|
const handleSetAllColumnsVisibility = useCallback(
|
||||||
|
(columnIds, isVisible) => {
|
||||||
|
const visibilityByColumnId = Object.fromEntries(columnIds.map((columnId) => [columnId, isVisible]));
|
||||||
|
onSetColumnsVisibility(visibilityByColumnId);
|
||||||
|
},
|
||||||
|
[onSetColumnsVisibility],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -140,19 +138,12 @@ const SettingPanel = ({
|
|||||||
data-active={isPinned}
|
data-active={isPinned}
|
||||||
disabled={!selectedColumnId}
|
disabled={!selectedColumnId}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onClick={() => {
|
onClick={handleTogglePin}>
|
||||||
if (!selectedColumnId) {
|
|
||||||
console.warn('[ColumnPin] клик Pin: selectedColumnId пустой');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId);
|
|
||||||
}}>
|
|
||||||
<Pin />
|
<Pin />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
|
|
||||||
|
|
||||||
<Divider orientation='vertical' flexItem />
|
<Divider orientation='vertical' flexItem />
|
||||||
<GroupByObject title='Строки'>
|
<GroupByObject title='Строки'>
|
||||||
{!isFormNoCanAddRow && (
|
{!isFormNoCanAddRow && (
|
||||||
@ -200,7 +191,12 @@ const SettingPanel = ({
|
|||||||
<Divider orientation='vertical' flexItem />
|
<Divider orientation='vertical' flexItem />
|
||||||
<GroupByObject title='Столбцы'>
|
<GroupByObject title='Столбцы'>
|
||||||
{columns ? (
|
{columns ? (
|
||||||
<CollapsibleTree columnTree={columns} onSelectNode={onToggleColumnVisibility} selectedIds={selectedColumnIds} />
|
<CollapsibleTree
|
||||||
|
columnTree={columns}
|
||||||
|
onSelectNode={onToggleColumnVisibility}
|
||||||
|
onSelectAllNodes={handleSetAllColumnsVisibility}
|
||||||
|
selectedIds={selectedColumnIds}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
@ -211,8 +207,7 @@ const SettingPanel = ({
|
|||||||
color='success'
|
color='success'
|
||||||
data-active={showOnlyCurrentStageColumns}
|
data-active={showOnlyCurrentStageColumns}
|
||||||
onClick={handleToggleCurrentStageColumns}
|
onClick={handleToggleCurrentStageColumns}
|
||||||
disabled={!columnsCurStage || columnsCurStage.length === 0}
|
disabled={!columnsCurStage || columnsCurStage.length === 0}>
|
||||||
>
|
|
||||||
{showOnlyCurrentStageColumns ? <AllStageColumns /> : <CurrentStageColumns />}
|
{showOnlyCurrentStageColumns ? <AllStageColumns /> : <CurrentStageColumns />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -223,7 +218,7 @@ const SettingPanel = ({
|
|||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
<Divider orientation='vertical' flexItem />
|
<Divider orientation='vertical' flexItem />
|
||||||
<GroupByObject title='Поиск'>
|
<GroupByObject title='Поиск'>
|
||||||
<SearchComponent onChange={onGlobalFilterChange} height='2.5rem' sx={{ height: '2.5rem' }} />
|
<SearchComponent onChange={onGlobalFilterChange} height='2.5rem' sx={searchSx} />
|
||||||
<Tooltip title='Поиск по колонкам'>
|
<Tooltip title='Поиск по колонкам'>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
@ -243,14 +238,14 @@ const SettingPanel = ({
|
|||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
disabled={!formId || !sheetName || isExporting}
|
disabled={!formId || !sheetName || isExporting}
|
||||||
text={isExporting ? 'Экспорт...' : 'Экспорт'}
|
text={isExporting ? 'Экспорт...' : 'Экспорт'}
|
||||||
sx={{ height: '2.5rem', minHeight: '2.5rem' }}
|
sx={exportButtonSx}
|
||||||
/>
|
/>
|
||||||
</GroupByObject>
|
</GroupByObject>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ValidationErrorsAlert errors={validationErrors} onNavigateToColumn={onNavigateToColumn} />
|
<ValidationErrorsAlert errors={validationErrors} onNavigateToColumn={onNavigateToColumn} />
|
||||||
</Panel >
|
</Panel>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingPanel;
|
export default memo(SettingPanel);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
|
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
|
||||||
import { Button, Menu, MenuItem, Stack } from '@mui/material';
|
import { Button, Menu, MenuItem, Stack } from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
|
|
||||||
const warningColors = {
|
const warningColors = {
|
||||||
bg: '#fff7e6',
|
bg: '#fff7e6',
|
||||||
@ -115,4 +115,4 @@ const ValidationErrorsAlert = ({ errors = [], onNavigateToColumn }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ValidationErrorsAlert;
|
export default memo(ValidationErrorsAlert);
|
||||||
|
|||||||
@ -1,32 +1,32 @@
|
|||||||
import { Box, Slider } from '@mui/material';
|
import { Box, Slider } from '@mui/material';
|
||||||
import { useState } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
const sliderContainerSx = { width: 200, height: '2.5rem', display: 'flex', alignItems: 'center', gap: 2 };
|
||||||
|
const zoomMarks = [
|
||||||
|
{ value: 0.5, label: '50%' },
|
||||||
|
{ value: 1, label: '100%' },
|
||||||
|
{ value: 2, label: '200%' },
|
||||||
|
];
|
||||||
|
|
||||||
const ZoomSlider = ({ onChange, width = 200, curScale }) => {
|
const ZoomSlider = ({ onChange, width = 200, curScale }) => {
|
||||||
const [scale, setScale] = useState(curScale);
|
const handleChange = (_event, newValue) => {
|
||||||
|
|
||||||
const handleChange = (event, newValue) => {
|
|
||||||
setScale(newValue);
|
|
||||||
if (onChange) onChange(newValue);
|
if (onChange) onChange(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width, height: '2.5rem', display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={width === 200 ? sliderContainerSx : { ...sliderContainerSx, width }}>
|
||||||
<Slider
|
<Slider
|
||||||
value={scale}
|
value={curScale}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
min={0.5}
|
min={0.5}
|
||||||
max={2}
|
max={2}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
valueLabelDisplay='auto'
|
valueLabelDisplay='auto'
|
||||||
valueLabelFormat={(value) => `${Math.round(value * 100)}%`}
|
valueLabelFormat={(value) => `${Math.round(value * 100)}%`}
|
||||||
marks={[
|
marks={zoomMarks}
|
||||||
{ value: 0.5, label: '50%' },
|
|
||||||
{ value: 1, label: '100%' },
|
|
||||||
{ value: 2, label: '200%' },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ZoomSlider;
|
export default memo(ZoomSlider);
|
||||||
|
|||||||
@ -1,75 +1,18 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { columnPinningDefault } from '../constants/columnConfig';
|
|
||||||
import ColumnResizer from './ColumnResizer';
|
import ColumnResizer from './ColumnResizer';
|
||||||
|
|
||||||
const getPinnedColumnIds = (table) => (table.getState().columnPinning.left || []).filter(Boolean);
|
|
||||||
|
|
||||||
const getSortedColumns = (table) => {
|
const getSortedColumns = (table) => {
|
||||||
const allLeafColumns = table.getAllLeafColumns();
|
return [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns(), ...table.getRightVisibleLeafColumns()];
|
||||||
const pinnedColumns = getPinnedColumnIds(table);
|
|
||||||
|
|
||||||
return [...allLeafColumns].sort((a, b) => {
|
|
||||||
const aIsPinned = pinnedColumns.includes(a.id);
|
|
||||||
const bIsPinned = pinnedColumns.includes(b.id);
|
|
||||||
|
|
||||||
if (aIsPinned && bIsPinned) {
|
|
||||||
return pinnedColumns.indexOf(a.id) - pinnedColumns.indexOf(b.id);
|
|
||||||
}
|
|
||||||
if (aIsPinned) return -1;
|
|
||||||
if (bIsPinned) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getColumnId = (header) => {
|
const getColumnPinnedStyles = (column) => {
|
||||||
//в header.id содержаться колонки по типу depth_idColParent_idChild, а в pinningColumn только idCol, которые закреплены
|
const pinningPosition = column.getIsPinned();
|
||||||
//header.id.split('_').at(-1) - получение самой нижней колонки
|
|
||||||
let columnId = header?.id?.match(/data\..*$/)?.[0] || header?.id || header;
|
|
||||||
columnPinningDefault.left.forEach((colPinned) => {
|
|
||||||
if (columnId.includes(colPinned)) {
|
|
||||||
columnId = colPinned;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return columnId;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ширина закреплённых дочерних колонок группового заголовка
|
return {
|
||||||
const getPinnedColumnsWidth = (header, pinningColumn) => {
|
isPinned: Boolean(pinningPosition),
|
||||||
let width = 0;
|
left: pinningPosition === 'left' ? column.getStart('left') : undefined,
|
||||||
for (const leaf of header.getLeafHeaders()) {
|
right: pinningPosition === 'right' ? column.getAfter('right') : undefined,
|
||||||
if (pinningColumn.includes(leaf.column.id)) {
|
};
|
||||||
width += leaf.column.getSize();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return width;
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculateLeftOffset = (table, columnOrHeader) => {
|
|
||||||
const columnId = getColumnId(columnOrHeader);
|
|
||||||
const pinnedColumns = getPinnedColumnIds(table);
|
|
||||||
const currentPinnedIndex = pinnedColumns.indexOf(columnId);
|
|
||||||
|
|
||||||
let leftOffset = 0;
|
|
||||||
|
|
||||||
if (currentPinnedIndex > 0) {
|
|
||||||
for (let i = 0; i < currentPinnedIndex; i++) {
|
|
||||||
const prevColumn = table.getColumn(pinnedColumns[i]);
|
|
||||||
if (prevColumn) {
|
|
||||||
leftOffset += prevColumn.getSize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return leftOffset;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isColumnPin = (table, header) => {
|
|
||||||
const pinningColumn = getPinnedColumnIds(table);
|
|
||||||
const columnId = getColumnId(header);
|
|
||||||
const isPinned = pinningColumn.includes(columnId);
|
|
||||||
return isPinned;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
||||||
@ -102,10 +45,9 @@ const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => {
|
const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => {
|
||||||
const isPinned = isColumnPin(table, column);
|
const { isPinned, left, right } = getColumnPinnedStyles(column);
|
||||||
const allLeafColumns = table.getAllLeafColumns();
|
const allLeafColumns = table.getAllLeafColumns();
|
||||||
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
|
const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id);
|
||||||
const leftOffset = calculateLeftOffset(table, column);
|
|
||||||
const selected = selectedColumnId === column.id;
|
const selected = selectedColumnId === column.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -133,7 +75,8 @@ const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) =
|
|||||||
border: '.063rem solid rgba(209, 213, 220, 1)',
|
border: '.063rem solid rgba(209, 213, 220, 1)',
|
||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
position: isPinned ? 'sticky' : 'relative',
|
position: isPinned ? 'sticky' : 'relative',
|
||||||
left: isPinned ? leftOffset : 'auto',
|
left,
|
||||||
|
right,
|
||||||
zIndex: selected ? 16 : isPinned ? 20 : 'auto',
|
zIndex: selected ? 16 : isPinned ? 20 : 'auto',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
@ -161,15 +104,12 @@ const getColorBrightness = (hexColor) => {
|
|||||||
return 0.299 * r + 0.587 * g + 0.114 * b;
|
return 0.299 * r + 0.587 * g + 0.114 * b;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
const HeaderCell = ({ header, table, pinningPosition, onClick, onChangeWidth }) => {
|
||||||
const column = header.column;
|
const column = header.column;
|
||||||
const pinningColumn = getPinnedColumnIds(table);
|
|
||||||
const isGroup = header.subHeaders?.length > 0;
|
|
||||||
const isPinned = isColumnPin(table, header);
|
|
||||||
const leftOffset = calculateLeftOffset(table, header);
|
|
||||||
const headerSize = header.getSize();
|
const headerSize = header.getSize();
|
||||||
const pinnedBandWidth = isGroup ? getPinnedColumnsWidth(header, pinningColumn) : 0;
|
const isPinned = Boolean(pinningPosition);
|
||||||
const splitGroup = isGroup && isPinned && pinnedBandWidth > 0 && pinnedBandWidth < headerSize;
|
const leftOffset = pinningPosition === 'left' ? header.getStart() : undefined;
|
||||||
|
const rightOffset = pinningPosition === 'right' ? table.getRightTotalSize() - header.getStart() - headerSize : undefined;
|
||||||
|
|
||||||
const customBgColor = column.columnDef?.muiTableHeadCellProps?.sx?.backgroundColor;
|
const customBgColor = column.columnDef?.muiTableHeadCellProps?.sx?.backgroundColor;
|
||||||
|
|
||||||
@ -185,34 +125,24 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
|||||||
onChangeWidth(header, size + deltaWidth);
|
onChangeWidth(header, size + deltaWidth);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Вычисляем отступ для текста внутри ячейки
|
return (
|
||||||
// Если колонка закреплена, текст должен быть привязан к левому краю ячейки
|
<div
|
||||||
// Если колонка не закреплена, текст должен начинаться после всех закрепленных колонок
|
style={{
|
||||||
const getTextLeftOffset = () => {
|
position: isPinned ? 'sticky' : 'relative',
|
||||||
if (isPinned) {
|
left: leftOffset,
|
||||||
return '0';
|
right: rightOffset,
|
||||||
}
|
zIndex: isPinned ? 21 : 'auto',
|
||||||
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
|
width: `${headerSize}px`,
|
||||||
// Получаем общую ширину всех закрепленных колонок
|
minWidth: `${headerSize}px`,
|
||||||
const pinnedColumns = getPinnedColumnIds(table);
|
maxWidth: `${headerSize}px`,
|
||||||
let totalPinnedWidth = 0;
|
flex: '0 0 auto',
|
||||||
for (const pinnedId of pinnedColumns) {
|
}}>
|
||||||
const pinnedCol = table.getColumn(pinnedId);
|
|
||||||
if (pinnedCol) {
|
|
||||||
totalPinnedWidth += pinnedCol.getSize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `${totalPinnedWidth}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCell = (width, textLeft) => (
|
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
<div
|
<div
|
||||||
onClick={() => onClick(header)}
|
onClick={() => onClick(header)}
|
||||||
style={{
|
style={{
|
||||||
width: width + 'px',
|
width: '100%',
|
||||||
minWidth: width + 'px',
|
minWidth: '100%',
|
||||||
maxWidth: width + 'px',
|
maxWidth: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
padding: '4px 6px',
|
padding: '4px 6px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
@ -231,9 +161,7 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
position: 'sticky',
|
position: 'relative',
|
||||||
left: textLeft,
|
|
||||||
right: 0,
|
|
||||||
paddingRight: '0.5rem',
|
paddingRight: '0.5rem',
|
||||||
paddingLeft: '0.5rem',
|
paddingLeft: '0.5rem',
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
@ -245,26 +173,6 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (splitGroup) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div style={{ position: 'sticky', left: leftOffset, zIndex: 21, flexShrink: 0 }}>{renderCell(pinnedBandWidth, '0')}</div>
|
|
||||||
<div style={{ position: 'relative', flexShrink: 0 }}>{renderCell(headerSize - pinnedBandWidth, getTextLeftOffset(true))}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: isPinned ? 'sticky' : 'relative',
|
|
||||||
left: isPinned ? leftOffset : 'auto',
|
|
||||||
zIndex: isPinned ? 21 : 'auto',
|
|
||||||
}}>
|
|
||||||
{renderCell(headerSize, getTextLeftOffset())}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const FilterRow = ({ table }) => {
|
const FilterRow = ({ table }) => {
|
||||||
@ -285,16 +193,15 @@ const FilterRow = ({ table }) => {
|
|||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
}}>
|
}}>
|
||||||
{filteredColumns.map((column) => (
|
{filteredColumns.map((column) => (
|
||||||
<FilterCell key={`filter-${column.id}`} column={column} table={table} />
|
<FilterCell key={`filter-${column.id}`} column={column} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Компонент ячейки фильтра
|
// Компонент ячейки фильтра
|
||||||
const FilterCell = ({ column, table }) => {
|
const FilterCell = ({ column }) => {
|
||||||
const isPinned = isColumnPin(table, column);
|
const { isPinned, left, right } = getColumnPinnedStyles(column);
|
||||||
const leftOffset = calculateLeftOffset(table, column);
|
|
||||||
|
|
||||||
// Определяем тип фильтра на основе колонки
|
// Определяем тип фильтра на основе колонки
|
||||||
const filterVariant = column.columnDef.filterVariant || 'text';
|
const filterVariant = column.columnDef.filterVariant || 'text';
|
||||||
@ -396,7 +303,8 @@ const FilterCell = ({ column, table }) => {
|
|||||||
border: '.063rem solid rgba(209, 213, 220, 1)',
|
border: '.063rem solid rgba(209, 213, 220, 1)',
|
||||||
borderTop: 'none',
|
borderTop: 'none',
|
||||||
position: isPinned ? 'sticky' : 'relative',
|
position: isPinned ? 'sticky' : 'relative',
|
||||||
left: isPinned ? leftOffset : 'auto',
|
left,
|
||||||
|
right,
|
||||||
zIndex: isPinned ? 19 : 'auto',
|
zIndex: isPinned ? 19 : 'auto',
|
||||||
}}>
|
}}>
|
||||||
{renderFilterInput()}
|
{renderFilterInput()}
|
||||||
@ -404,7 +312,7 @@ const FilterCell = ({ column, table }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData }) => {
|
const TableHeadComponent = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData }) => {
|
||||||
const handleHeaderClick = (header) => {
|
const handleHeaderClick = (header) => {
|
||||||
if (!header.subHeaders?.length && header.column?.id) {
|
if (!header.subHeaders?.length && header.column?.id) {
|
||||||
onColumnSelect?.(header.column.id);
|
onColumnSelect?.(header.column.id);
|
||||||
@ -415,6 +323,13 @@ export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWid
|
|||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headerSections = [
|
||||||
|
{ groups: table.getLeftHeaderGroups(), pinningPosition: 'left' },
|
||||||
|
{ groups: table.getCenterHeaderGroups(), pinningPosition: undefined },
|
||||||
|
{ groups: table.getRightHeaderGroups(), pinningPosition: 'right' },
|
||||||
|
];
|
||||||
|
const headerRowsCount = Math.max(...headerSections.map(({ groups }) => groups.length));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -428,17 +343,26 @@ export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWid
|
|||||||
|
|
||||||
{table.getState().showColumnFilters && <FilterRow table={table} />}
|
{table.getState().showColumnFilters && <FilterRow table={table} />}
|
||||||
|
|
||||||
{/* Существующие строки заголовков */}
|
{/* Pinned и обычные заголовки строятся отдельными группами. */}
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{Array.from({ length: headerRowsCount }, (_, rowIndex) => (
|
||||||
<div key={headerGroup.id} style={{ display: 'flex' }}>
|
<div key={`header-row-${rowIndex}`} style={{ display: 'flex' }}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerSections.flatMap(({ groups, pinningPosition }) =>
|
||||||
<React.Fragment key={header.id}>
|
(groups[rowIndex]?.headers || []).map((header) => (
|
||||||
<HeaderCell header={header} table={table} onClick={handleHeaderClick} onChangeWidth={onChangeWidth} />
|
<HeaderCell
|
||||||
</React.Fragment>
|
key={`${pinningPosition || 'center'}-${header.id}`}
|
||||||
))}
|
header={header}
|
||||||
|
table={table}
|
||||||
|
pinningPosition={pinningPosition}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
onChangeWidth={onChangeWidth}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TableHead = React.memo(TableHeadComponent);
|
||||||
|
|||||||
@ -279,7 +279,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_i_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_i_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q1.adj_by_items',
|
accessorKey: 'data.q1.adj_by_items',
|
||||||
columnLetter: 'E',
|
columnLetter: 'E',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -292,7 +292,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q1.adj_increase',
|
accessorKey: 'data.q1.adj_increase',
|
||||||
columnLetter: 'F',
|
columnLetter: 'F',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -368,7 +368,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За март',
|
||||||
accessorKey: 'data.q1.m3',
|
accessorKey: 'data.q1.m3',
|
||||||
columnLetter: 'J',
|
columnLetter: 'J',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -462,7 +462,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_ii_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_ii_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q2.adj_by_items',
|
accessorKey: 'data.q2.adj_by_items',
|
||||||
columnLetter: 'O',
|
columnLetter: 'O',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -475,7 +475,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q2.adj_increase',
|
accessorKey: 'data.q2.adj_increase',
|
||||||
columnLetter: 'P',
|
columnLetter: 'P',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -551,7 +551,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За июнь',
|
||||||
accessorKey: 'data.q2.m3',
|
accessorKey: 'data.q2.m3',
|
||||||
columnLetter: 'T',
|
columnLetter: 'T',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -645,7 +645,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iii_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iii_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q3.adj_by_items',
|
accessorKey: 'data.q3.adj_by_items',
|
||||||
columnLetter: 'Y',
|
columnLetter: 'Y',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -658,7 +658,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q3.adj_increase',
|
accessorKey: 'data.q3.adj_increase',
|
||||||
columnLetter: 'Z',
|
columnLetter: 'Z',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -734,7 +734,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За сентябрь',
|
||||||
accessorKey: 'data.q3.m3',
|
accessorKey: 'data.q3.m3',
|
||||||
columnLetter: 'AD',
|
columnLetter: 'AD',
|
||||||
size: 150,
|
size: 150,
|
||||||
@ -828,7 +828,7 @@ export const config = {
|
|||||||
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iv_kvartale_korrektirovka_limita',
|
accessorKey: 'otchet_ob_ispolnenii_tekuschikh_raskhodov_v_iv_kvartale_korrektirovka_limita',
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'По статьям сметы *',
|
header: 'По статьям сметы*',
|
||||||
accessorKey: 'data.q4.adj_by_items',
|
accessorKey: 'data.q4.adj_by_items',
|
||||||
columnLetter: 'AI',
|
columnLetter: 'AI',
|
||||||
size: 180,
|
size: 180,
|
||||||
@ -841,7 +841,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Увеличение сметы **',
|
header: 'Увеличение сметы**',
|
||||||
accessorKey: 'data.q4.adj_increase',
|
accessorKey: 'data.q4.adj_increase',
|
||||||
columnLetter: 'AJ',
|
columnLetter: 'AJ',
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -917,7 +917,7 @@ export const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'За',
|
header: 'За декабрь',
|
||||||
accessorKey: 'data.q4.m3',
|
accessorKey: 'data.q4.m3',
|
||||||
columnLetter: 'AN',
|
columnLetter: 'AN',
|
||||||
size: 150,
|
size: 150,
|
||||||
|
|||||||
@ -28,6 +28,10 @@ export const BASE_TABLE_CONFIG = {
|
|||||||
defaultColumn: { filterFn: 'contains' },
|
defaultColumn: { filterFn: 'contains' },
|
||||||
columnResizeMode: 'onChange',
|
columnResizeMode: 'onChange',
|
||||||
localization: {
|
localization: {
|
||||||
|
expand: 'Раскрыть',
|
||||||
|
expandAll: 'Раскрыть все',
|
||||||
|
collapse: 'Свернуть',
|
||||||
|
collapseAll: 'Свернуть все',
|
||||||
noRecordsToDisplay: <span>Нет данных для отображения</span>,
|
noRecordsToDisplay: <span>Нет данных для отображения</span>,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,116 +1,162 @@
|
|||||||
import {
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
||||||
createContext,
|
import { toast } from 'react-toastify';
|
||||||
useCallback,
|
import { useWebSocket } from '../hooks/useWebSocket';
|
||||||
useContext,
|
import { getRowId } from '../utils/rowUtils';
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import { useWebSocket } from "../hooks/useWebSocket";
|
|
||||||
import { getRowId } from "../utils/rowUtils";
|
|
||||||
|
|
||||||
const RealtimeContext = createContext(null);
|
const RealtimeActionsContext = createContext(null);
|
||||||
|
const RealtimeConnectionContext = createContext(null);
|
||||||
|
const RealtimeVspOptionsContext = createContext(null);
|
||||||
|
const LockedCellsStoreContext = createContext(null);
|
||||||
|
|
||||||
export const useRealtime = () => {
|
const useRequiredContext = (context, hookName) => {
|
||||||
const context = useContext(RealtimeContext);
|
const value = useContext(context);
|
||||||
if (!context) {
|
if (value === null) {
|
||||||
throw new Error("useRealtime must be used within RealtimeProvider");
|
throw new Error(`${hookName} must be used within RealtimeProvider`);
|
||||||
}
|
}
|
||||||
return context;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RealtimeProvider = ({
|
const createLockedCellsStore = () => {
|
||||||
children,
|
let cells = new Set();
|
||||||
formId,
|
const listenersByCell = new Map();
|
||||||
sheetName,
|
|
||||||
direction,
|
const subscribeCell = (cellKey, listener) => {
|
||||||
year,
|
const listeners = listenersByCell.get(cellKey) || new Set();
|
||||||
userId,
|
listeners.add(listener);
|
||||||
isProject = false,
|
listenersByCell.set(cellKey, listeners);
|
||||||
}) => {
|
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener);
|
||||||
|
if (listeners.size === 0) listenersByCell.delete(cellKey);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCells = (nextValue) => {
|
||||||
|
const currentCells = [...cells];
|
||||||
|
const resolvedValue = typeof nextValue === 'function' ? nextValue(currentCells) : nextValue;
|
||||||
|
const nextCells = new Set(resolvedValue || []);
|
||||||
|
const changedCells = new Set([
|
||||||
|
...[...cells].filter((cellKey) => !nextCells.has(cellKey)),
|
||||||
|
...[...nextCells].filter((cellKey) => !cells.has(cellKey)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (changedCells.size === 0) return;
|
||||||
|
cells = nextCells;
|
||||||
|
changedCells.forEach((cellKey) => {
|
||||||
|
listenersByCell.get(cellKey)?.forEach((listener) => listener());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribeCell,
|
||||||
|
setCells,
|
||||||
|
hasCell: (cellKey) => cells.has(cellKey),
|
||||||
|
getCells: () => [...cells],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRealtimeActions = () => useRequiredContext(RealtimeActionsContext, 'useRealtimeActions');
|
||||||
|
|
||||||
|
export const useRealtimeConnection = () => useRequiredContext(RealtimeConnectionContext, 'useRealtimeConnection');
|
||||||
|
|
||||||
|
export const useVspOptions = () => useRequiredContext(RealtimeVspOptionsContext, 'useVspOptions');
|
||||||
|
|
||||||
|
export const useCellLock = (cellKey) => {
|
||||||
|
const store = useRequiredContext(LockedCellsStoreContext, 'useCellLock');
|
||||||
|
const subscribe = useCallback((listener) => store.subscribeCell(cellKey, listener), [cellKey, store]);
|
||||||
|
const getSnapshot = useCallback(() => store.hasCell(cellKey), [cellKey, store]);
|
||||||
|
|
||||||
|
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRealtime = () => {
|
||||||
|
const actions = useRealtimeActions();
|
||||||
|
const connection = useRealtimeConnection();
|
||||||
|
const vspOptions = useVspOptions();
|
||||||
|
|
||||||
|
return { ...actions, ...connection, vspOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RealtimeProvider = ({ children, formId, sheetName, direction, year, isProject = false }) => {
|
||||||
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
||||||
const wsUrl = `${(
|
const wsUrl = `${(
|
||||||
process.env.REACT_APP_API_URL ||
|
process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || 'ws://localhost:8000'
|
||||||
process.env.REACT_APP_API_URL ||
|
).replace(/^http/, 'ws')}${
|
||||||
process.env.REACT_APP_ROOT_PATH ||
|
|
||||||
"ws://localhost:8000"
|
|
||||||
).replace(/^http/, "ws")}${
|
|
||||||
!isProject
|
!isProject
|
||||||
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`
|
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}`
|
||||||
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`
|
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
//для ячеек которые редактируются другими пользователями
|
|
||||||
const [lockedCells, setLockedCells] = useState([]);
|
|
||||||
const [vspOptions, setVspOptions] = useState([]);
|
const [vspOptions, setVspOptions] = useState([]);
|
||||||
|
const lockedCellsStoreRef = useRef(null);
|
||||||
|
if (lockedCellsStoreRef.current === null) {
|
||||||
|
lockedCellsStoreRef.current = createLockedCellsStore();
|
||||||
|
}
|
||||||
|
const lockedCellsStore = lockedCellsStoreRef.current;
|
||||||
|
|
||||||
const handleMessage = useCallback((data) => {
|
const handleMessage = useCallback((data) => {
|
||||||
// Обработка ошибок
|
// Обработка ошибок
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.log(data.error.message);
|
console.log(data.error.message);
|
||||||
toast.error(`Server error:${data.error.message}` || "");
|
toast.error(`Server error:${data.error.message}` || '');
|
||||||
onErrorRef.current?.(data.error);
|
onErrorRef.current?.(data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.event) {
|
switch (data.event) {
|
||||||
case "cell_edit_start":
|
case 'cell_edit_start':
|
||||||
console.log("Cell edit started:", data);
|
console.log('Cell edit started:', data);
|
||||||
if (onCellEditStartRef.current) {
|
if (onCellEditStartRef.current) {
|
||||||
onCellEditStartRef.current(data);
|
onCellEditStartRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "cell_edit_end":
|
case 'cell_edit_end':
|
||||||
console.log("Cell edit ended:", data);
|
console.log('Cell edit ended:', data);
|
||||||
if (onCellEditEndRef.current) {
|
if (onCellEditEndRef.current) {
|
||||||
onCellEditEndRef.current(data);
|
onCellEditEndRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "cell_updated":
|
case 'cell_updated':
|
||||||
if (data.result && onCellUpdateRef.current) {
|
if (data.result && onCellUpdateRef.current) {
|
||||||
const updatedCells = Array.isArray(data.result)
|
const updatedCells = Array.isArray(data.result) ? data.result : [data.result];
|
||||||
? data.result
|
|
||||||
: [data.result];
|
|
||||||
onCellUpdateRef.current(updatedCells);
|
onCellUpdateRef.current(updatedCells);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "row_added":
|
case 'row_added':
|
||||||
if (data.result && onRowAddRef.current) {
|
if (data.result && onRowAddRef.current) {
|
||||||
onRowAddRef.current(data);
|
onRowAddRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "row_deleted":
|
case 'row_deleted':
|
||||||
if (data.result && onRowDeleteRef.current) {
|
if (data.result && onRowDeleteRef.current) {
|
||||||
onRowDeleteRef.current(data);
|
onRowDeleteRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "program_added":
|
case 'program_added':
|
||||||
console.log("Program added:", data);
|
console.log('Program added:', data);
|
||||||
if (data.result && onProgramAddRef.current) {
|
if (data.result && onProgramAddRef.current) {
|
||||||
onProgramAddRef.current(data);
|
onProgramAddRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "project_added":
|
case 'project_added':
|
||||||
console.log("Project added:", data);
|
console.log('Project added:', data);
|
||||||
if (data.result && onProjectAddRef.current) {
|
if (data.result && onProjectAddRef.current) {
|
||||||
onProjectAddRef.current(data);
|
onProjectAddRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log("Unknown event:", data.event);
|
console.log('Unknown event:', data.event);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } =
|
const { isConnectedRef, isConnected, sendMessage } = useWebSocket(wsUrl, handleMessage);
|
||||||
useWebSocket(wsUrl, handleMessage);
|
|
||||||
|
|
||||||
const onCellUpdateRef = useRef(null);
|
const onCellUpdateRef = useRef(null);
|
||||||
const onRowAddRef = useRef(null);
|
const onRowAddRef = useRef(null);
|
||||||
@ -122,21 +168,20 @@ export const RealtimeProvider = ({
|
|||||||
const onCellEditStartRef = useRef(null);
|
const onCellEditStartRef = useRef(null);
|
||||||
const onCellEditEndRef = useRef(null);
|
const onCellEditEndRef = useRef(null);
|
||||||
const authAttemptedRef = useRef(false);
|
const authAttemptedRef = useRef(false);
|
||||||
const reconnectTimerRef = useRef(null);
|
|
||||||
|
|
||||||
// Функция для получения токена из localStorage
|
// Функция для получения токена из localStorage
|
||||||
const getAccessToken = useCallback(() => {
|
const getAccessToken = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem('access_token');
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.warn("No access token found in localStorage");
|
console.warn('No access token found in localStorage');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading token from localStorage:", error);
|
console.error('Error reading token from localStorage:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -144,13 +189,13 @@ export const RealtimeProvider = ({
|
|||||||
const sendLogin = useCallback(() => {
|
const sendLogin = useCallback(() => {
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.error("Cannot login: no token available");
|
console.error('Cannot login: no token available');
|
||||||
onErrorRef.current?.("Токен авторизации не найден");
|
onErrorRef.current?.('Токен авторизации не найден');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginMessage = {
|
const loginMessage = {
|
||||||
event: "user_login",
|
event: 'user_login',
|
||||||
data: { token },
|
data: { token },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,10 +205,11 @@ export const RealtimeProvider = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected && !authAttemptedRef.current) {
|
if (isConnected && !authAttemptedRef.current) {
|
||||||
authAttemptedRef.current = true;
|
authAttemptedRef.current = true;
|
||||||
console.log("WebSocket connected, sending login...");
|
console.log('WebSocket connected, sending login...');
|
||||||
setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
sendLogin();
|
sendLogin();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}, [isConnected, sendLogin]);
|
}, [isConnected, sendLogin]);
|
||||||
|
|
||||||
@ -173,27 +219,29 @@ export const RealtimeProvider = ({
|
|||||||
}
|
}
|
||||||
}, [isConnected]);
|
}, [isConnected]);
|
||||||
|
|
||||||
const addCommonField = (message) => {
|
const addCommonField = useCallback(
|
||||||
if (isProject) {
|
(message) => {
|
||||||
message.report_type = sheetName;
|
if (isProject) {
|
||||||
message.project_id = formId;
|
message.report_type = sheetName;
|
||||||
message.year = year;
|
message.project_id = formId;
|
||||||
} else {
|
message.year = year;
|
||||||
message.sheet = sheetName;
|
} else {
|
||||||
message.form_id = formId;
|
message.sheet = sheetName;
|
||||||
message.direction = direction;
|
message.form_id = formId;
|
||||||
}
|
message.direction = direction;
|
||||||
return message;
|
}
|
||||||
};
|
return message;
|
||||||
|
},
|
||||||
|
[direction, formId, isProject, sheetName, year],
|
||||||
|
);
|
||||||
|
|
||||||
// Функция для начала редактирования ячейки
|
// Функция для начала редактирования ячейки
|
||||||
const startEditing = useCallback(
|
const startEditing = useCallback(
|
||||||
async (row, column) => {
|
(row, column) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
|
||||||
|
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef.current) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.('WebSocket не подключен');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +249,7 @@ export const RealtimeProvider = ({
|
|||||||
const colId = columnId.slice(5); // убираем префикс "data."
|
const colId = columnId.slice(5); // убираем префикс "data."
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "cell_edit_start",
|
event: 'cell_edit_start',
|
||||||
data: {
|
data: {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
column: colId,
|
column: colId,
|
||||||
@ -215,17 +263,16 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage],
|
[isConnectedRef, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Функция для завершения редактирования ячейки
|
// Функция для завершения редактирования ячейки
|
||||||
const endEditing = useCallback(
|
const endEditing = useCallback(
|
||||||
async (row, column) => {
|
(row, column) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
|
||||||
|
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef.current) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.('WebSocket не подключен');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,7 +280,7 @@ export const RealtimeProvider = ({
|
|||||||
const colId = columnId.slice(5);
|
const colId = columnId.slice(5);
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "cell_edit_end",
|
event: 'cell_edit_end',
|
||||||
data: {
|
data: {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
column: colId,
|
column: colId,
|
||||||
@ -247,22 +294,21 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage],
|
[isConnectedRef, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateCell = useCallback(
|
const updateCell = useCallback(
|
||||||
async (row, column, value) => {
|
(row, column, value) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
if (!isConnectedRef.current) {
|
||||||
if (!isConnectedRef) {
|
onErrorRef.current?.('WebSocket не подключен');
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineId = getRowId(row);
|
const lineId = getRowId(row);
|
||||||
const colId = columnId.slice(5);
|
const colId = columnId.slice(5);
|
||||||
const message = {
|
const message = {
|
||||||
event: "cell_updated",
|
event: 'cell_updated',
|
||||||
data: {
|
data: {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
line_id_code: row.id,
|
line_id_code: row.id,
|
||||||
@ -278,79 +324,79 @@ export const RealtimeProvider = ({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage],
|
[isConnectedRef, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowData) => {
|
(rowData) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef.current) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "row_added",
|
event: 'row_added',
|
||||||
data: rowData,
|
data: rowData,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[addCommonField, isConnectedRef, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addProject = useCallback(
|
const addProject = useCallback(
|
||||||
async (rowData) => {
|
(rowData) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef.current) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "project_added",
|
event: 'project_added',
|
||||||
data: rowData,
|
data: rowData,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[addCommonField, isConnectedRef, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addProgram = useCallback(
|
const addProgram = useCallback(
|
||||||
async (rowData) => {
|
(rowData) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef.current) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "program_added",
|
event: 'program_added',
|
||||||
data: rowData,
|
data: rowData,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[addCommonField, isConnectedRef, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteRow = useCallback(
|
const deleteRow = useCallback(
|
||||||
async (rowId) => {
|
(rowId) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef.current) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "row_deleted",
|
event: 'row_deleted',
|
||||||
data: { row_id: rowId },
|
data: { row_id: rowId },
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[addCommonField, isConnectedRef, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const subscribeToCellUpdates = useCallback((callback) => {
|
const subscribeToCellUpdates = useCallback((callback) => {
|
||||||
@ -417,42 +463,65 @@ export const RealtimeProvider = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const retryLogin = useCallback(() => {
|
const retryLogin = useCallback(() => {
|
||||||
if (isConnected) {
|
if (isConnectedRef.current) {
|
||||||
authAttemptedRef.current = false;
|
authAttemptedRef.current = false;
|
||||||
sendLogin();
|
sendLogin();
|
||||||
}
|
}
|
||||||
}, [isConnected, sendLogin]);
|
}, [isConnectedRef, sendLogin]);
|
||||||
|
|
||||||
|
const actions = useMemo(
|
||||||
|
() => ({
|
||||||
|
startEditing,
|
||||||
|
endEditing,
|
||||||
|
updateCell,
|
||||||
|
addRow,
|
||||||
|
addProgram,
|
||||||
|
addProject,
|
||||||
|
deleteRow,
|
||||||
|
subscribeToRowAdds,
|
||||||
|
subscribeToRowDeletes,
|
||||||
|
subscribeToProgramAdds,
|
||||||
|
subscribeToProjectAdds,
|
||||||
|
subscribeToErrors,
|
||||||
|
subscribeToAuthSuccess,
|
||||||
|
subscribeToCellEditStart,
|
||||||
|
subscribeToCellEditEnd,
|
||||||
|
subscribeToCellUpdates,
|
||||||
|
retryLogin,
|
||||||
|
setLockedCells: lockedCellsStore.setCells,
|
||||||
|
setVspOptions,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
addProgram,
|
||||||
|
addProject,
|
||||||
|
addRow,
|
||||||
|
deleteRow,
|
||||||
|
endEditing,
|
||||||
|
lockedCellsStore,
|
||||||
|
retryLogin,
|
||||||
|
startEditing,
|
||||||
|
subscribeToAuthSuccess,
|
||||||
|
subscribeToCellEditEnd,
|
||||||
|
subscribeToCellEditStart,
|
||||||
|
subscribeToCellUpdates,
|
||||||
|
subscribeToErrors,
|
||||||
|
subscribeToProgramAdds,
|
||||||
|
subscribeToProjectAdds,
|
||||||
|
subscribeToRowAdds,
|
||||||
|
subscribeToRowDeletes,
|
||||||
|
updateCell,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const connection = useMemo(() => ({ isConnected }), [isConnected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RealtimeContext.Provider
|
<RealtimeActionsContext.Provider value={actions}>
|
||||||
value={{
|
<RealtimeConnectionContext.Provider value={connection}>
|
||||||
isConnected,
|
<RealtimeVspOptionsContext.Provider value={vspOptions}>
|
||||||
error: lastError,
|
<LockedCellsStoreContext.Provider value={lockedCellsStore}>{children}</LockedCellsStoreContext.Provider>
|
||||||
startEditing,
|
</RealtimeVspOptionsContext.Provider>
|
||||||
endEditing,
|
</RealtimeConnectionContext.Provider>
|
||||||
updateCell,
|
</RealtimeActionsContext.Provider>
|
||||||
addRow,
|
|
||||||
addProgram,
|
|
||||||
addProject,
|
|
||||||
deleteRow,
|
|
||||||
subscribeToRowAdds,
|
|
||||||
subscribeToRowDeletes,
|
|
||||||
subscribeToProgramAdds,
|
|
||||||
subscribeToProjectAdds,
|
|
||||||
subscribeToErrors,
|
|
||||||
subscribeToAuthSuccess,
|
|
||||||
subscribeToCellEditStart,
|
|
||||||
subscribeToCellEditEnd,
|
|
||||||
subscribeToCellUpdates,
|
|
||||||
retryLogin,
|
|
||||||
lockedCells,
|
|
||||||
setLockedCells,
|
|
||||||
vspOptions,
|
|
||||||
setVspOptions,
|
|
||||||
releaseAllLocks: () => releaseAllLocks(userId),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</RealtimeContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -33,7 +33,10 @@ export const useColumnSettings = (tableKey) => {
|
|||||||
}, [tableKey]);
|
}, [tableKey]);
|
||||||
|
|
||||||
const handlePinColumn = useCallback((colId) => {
|
const handlePinColumn = useCallback((colId) => {
|
||||||
setColumnPinning((prev) => ({ ...prev, left: [...prev.left, colId] }));
|
setColumnPinning((prev) => {
|
||||||
|
if (!colId || prev.left.includes(colId)) return prev;
|
||||||
|
return { ...prev, left: [...prev.left, colId] };
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUnpinColumn = useCallback((colId) => {
|
const handleUnpinColumn = useCallback((colId) => {
|
||||||
@ -63,7 +66,18 @@ export const useColumnSettings = (tableKey) => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
setColumnVisibility((prev) => ({ ...prev, ...leafCols }));
|
setColumnVisibility((prev) => {
|
||||||
|
const hasChanges = Object.entries(leafCols).some(([columnId, isVisible]) => prev[columnId] !== isVisible);
|
||||||
|
return hasChanges ? { ...prev, ...leafCols } : prev;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSetColumnsVisibility = useCallback((visibilityByColumnId) => {
|
||||||
|
setColumnVisibility((prev) => {
|
||||||
|
const entries = Object.entries(visibilityByColumnId);
|
||||||
|
const hasChanges = entries.some(([columnId, isVisible]) => prev[columnId] !== isVisible);
|
||||||
|
return hasChanges ? { ...prev, ...visibilityByColumnId } : prev;
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Опционально: функция для сброса всех настроек
|
// Опционально: функция для сброса всех настроек
|
||||||
@ -84,6 +98,7 @@ export const useColumnSettings = (tableKey) => {
|
|||||||
handleUnpinColumn,
|
handleUnpinColumn,
|
||||||
handleSetColumnWidth,
|
handleSetColumnWidth,
|
||||||
handleToggleColumnVisibility,
|
handleToggleColumnVisibility,
|
||||||
|
handleSetColumnsVisibility,
|
||||||
resetAllSettings, // добавляем функцию сброса
|
resetAllSettings, // добавляем функцию сброса
|
||||||
tableKey, // опционально возвращаем tableKey
|
tableKey, // опционально возвращаем tableKey
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,29 +1,54 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from "react-toastify";
|
import { toast } from 'react-toastify';
|
||||||
import { FormsSheetApi } from "../../../api/form_sheet";
|
import { FormsSheetApi } from '../../../api/form_sheet';
|
||||||
import { ProjectsApi } from "../../../api/projects";
|
import { ProjectsApi } from '../../../api/projects';
|
||||||
import { useRealtime } from "../contexts/RealtimeContext";
|
import { useRealtimeActions, useRealtimeConnection } from '../contexts/RealtimeContext';
|
||||||
import {
|
import { addRootRow, deleteRow, insertCell, insertProjectRow, updateCells } from '../utils/cellUtils';
|
||||||
addRootRow,
|
import { getParentId, isVspNewRow } from '../utils/rowUtils';
|
||||||
deleteRow,
|
|
||||||
insertCell,
|
const ROW_TYPE_LEVELS = {
|
||||||
insertProjectRow,
|
ROOT: 0,
|
||||||
updateCells,
|
GROUP: 1,
|
||||||
} from "../utils/cellUtils";
|
ITEM: 2,
|
||||||
import { getParentId, isVspNewRow } from "../utils/rowUtils";
|
SUB_ITEM: 3,
|
||||||
|
INPUT: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildHierarchy = (items) => {
|
||||||
|
const result = [];
|
||||||
|
const stack = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const currentLevel = ROW_TYPE_LEVELS[item.row_type] ?? 999;
|
||||||
|
|
||||||
|
while (stack.length > 0 && (ROW_TYPE_LEVELS[stack.at(-1).row_type] ?? 999) >= currentLevel) {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = stack.at(-1);
|
||||||
|
const newNode = { ...item, subRows: [] };
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
parent.subRows.push(newNode);
|
||||||
|
} else {
|
||||||
|
result.push(newNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.row_type !== 'INPUT') {
|
||||||
|
stack.push(newNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
||||||
const [data, setData] = useState([]);
|
const [data, setData] = useState([]);
|
||||||
const [columnsCurStage, setColumnsCurStage] = useState([]);
|
const [columnsCurStage, setColumnsCurStage] = useState([]);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [editingCells, setEditingCells] = useState({});
|
const [editingCells, setEditingCells] = useState({});
|
||||||
const dataRef = useRef(data);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnected: wsConnected,
|
|
||||||
error: wsError,
|
|
||||||
subscribeToCellUpdates,
|
subscribeToCellUpdates,
|
||||||
subscribeToRowAdds,
|
subscribeToRowAdds,
|
||||||
subscribeToProgramAdds,
|
subscribeToProgramAdds,
|
||||||
@ -33,62 +58,19 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
subscribeToCellEditStart,
|
subscribeToCellEditStart,
|
||||||
subscribeToCellEditEnd,
|
subscribeToCellEditEnd,
|
||||||
setLockedCells,
|
setLockedCells,
|
||||||
} = useRealtime();
|
} = useRealtimeActions();
|
||||||
|
const { isConnected: wsConnected } = useRealtimeConnection();
|
||||||
const formatedToSubRows = useCallback((depth, oldData) => {
|
|
||||||
if (depth === 0) return oldData;
|
|
||||||
|
|
||||||
const dataFiltered = oldData.filter((d) => d.depth < depth);
|
|
||||||
|
|
||||||
for (let i = 0; i < dataFiltered.length; i++) {
|
|
||||||
const curRow = dataFiltered[i];
|
|
||||||
|
|
||||||
// Находим следующий элемент на том же или более высоком уровне
|
|
||||||
let nextRow = null;
|
|
||||||
for (let j = i + 1; j < dataFiltered.length; j++) {
|
|
||||||
if (dataFiltered[j].depth <= curRow.depth) {
|
|
||||||
nextRow = dataFiltered[j];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Находим всех потомков для текущей строки
|
|
||||||
const subRows = oldData.filter((d) => {
|
|
||||||
// Потомок должен иметь глубину больше чем у родителя
|
|
||||||
if (d.depth <= curRow.depth) return false;
|
|
||||||
|
|
||||||
// Потомок должен идти после родителя по sort_order
|
|
||||||
if (d.sort_order <= curRow.sort_order) return false;
|
|
||||||
|
|
||||||
// Если есть следующий элемент на том же уровне - потомок должен быть до него
|
|
||||||
if (nextRow && d.sort_order >= nextRow.sort_order) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (subRows.length > 0) {
|
|
||||||
curRow.subRows = subRows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Рекурсивно обрабатываем следующий уровень
|
|
||||||
// Находим максимальную глубину в текущем dataFiltered
|
|
||||||
const maxDepth = Math.max(...dataFiltered.map((d) => d.depth));
|
|
||||||
if (maxDepth > 0) {
|
|
||||||
return formatedToSubRows(maxDepth, dataFiltered);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataFiltered;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Загрузка начальных данных
|
// Загрузка начальных данных
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
if (formType === "PROJECT") {
|
if (formType === 'PROJECT') {
|
||||||
res = await ProjectsApi.getTableData(formId, sheetName, year);
|
res = await ProjectsApi.getTableData(formId, sheetName, year);
|
||||||
} else if (direction !== null) {
|
} else if (direction !== null) {
|
||||||
res = await FormsSheetApi.get(formId, sheetName, {
|
res = await FormsSheetApi.get(formId, sheetName, {
|
||||||
@ -97,15 +79,12 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
} else {
|
} else {
|
||||||
res = await FormsSheetApi.get(formId, sheetName);
|
res = await FormsSheetApi.get(formId, sheetName);
|
||||||
}
|
}
|
||||||
|
if (isCancelled) return;
|
||||||
|
|
||||||
const data = res.result.filter((d) => d.depth > -1);
|
const data = res.result.filter((d) => d.depth > -1);
|
||||||
const curStageColumns = res.result.filter((d) => d.depth === -1)[0];
|
const curStageColumns = res.result.filter((d) => d.depth === -1)[0];
|
||||||
if (curStageColumns) {
|
const stageColumns = (curStageColumns?.data?.editable || []).map((column) => `data.${column.column}`);
|
||||||
const cols = [];
|
setColumnsCurStage(stageColumns);
|
||||||
for (const col of curStageColumns.data.editable) {
|
|
||||||
cols.push(`data.${col.column}`);
|
|
||||||
}
|
|
||||||
setColumnsCurStage(cols);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
setData([]);
|
setData([]);
|
||||||
@ -115,74 +94,18 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
const hierarchicalData = buildHierarchy(data);
|
const hierarchicalData = buildHierarchy(data);
|
||||||
setData(hierarchicalData);
|
setData(hierarchicalData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load initial data:", err);
|
if (!isCancelled) console.error('Failed to load initial data:', err);
|
||||||
setError(err.message);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (!isCancelled) setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
}, [formId, sheetName]);
|
|
||||||
|
|
||||||
const buildHierarchy = (items) => {
|
return () => {
|
||||||
const result = [];
|
isCancelled = true;
|
||||||
const stack = [];
|
|
||||||
|
|
||||||
const getTypeLevel = (type) => {
|
|
||||||
const levels = {
|
|
||||||
ROOT: 0,
|
|
||||||
GROUP: 1,
|
|
||||||
ITEM: 2,
|
|
||||||
SUB_ITEM: 3,
|
|
||||||
INPUT: 4,
|
|
||||||
};
|
|
||||||
return levels[type] ?? 999;
|
|
||||||
};
|
};
|
||||||
|
}, [direction, formId, formType, sheetName, year]);
|
||||||
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);
|
|
||||||
}, [wsConnected]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setError(wsError);
|
|
||||||
}, [wsError]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dataRef.current = data;
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
// Подписка на обновления WebSocket
|
// Подписка на обновления WebSocket
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -213,12 +136,12 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
[[], null],
|
[[], null],
|
||||||
);
|
);
|
||||||
if (newRow === null) {
|
if (newRow === null) {
|
||||||
toast.error("Ошибка получения данных");
|
toast.error('Ошибка получения данных');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData((prevData) => {
|
setData((prevData) => {
|
||||||
let data = structuredClone(prevData);
|
let data = structuredClone(prevData);
|
||||||
if (prevData[0]?.row_type !== "ROOT" && updates[0][0] === 'ROOT') {
|
if (prevData[0]?.row_type !== 'ROOT' && updates[0][0] === 'ROOT') {
|
||||||
const newRowRoot = updates[0];
|
const newRowRoot = updates[0];
|
||||||
data = addRootRow(data, newRowRoot);
|
data = addRootRow(data, newRowRoot);
|
||||||
}
|
}
|
||||||
@ -233,7 +156,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
const program_id = data.data.program_id;
|
const program_id = data.data.program_id;
|
||||||
const [updatedCells, newRow] = updates.reduce(
|
const [updatedCells, newRow] = updates.reduce(
|
||||||
(res, row) => {
|
(res, row) => {
|
||||||
if (row[0] === "ITEM") {
|
if (row[0] === 'ITEM') {
|
||||||
res[1] = row;
|
res[1] = row;
|
||||||
} else {
|
} else {
|
||||||
res[0].push(row);
|
res[0].push(row);
|
||||||
@ -243,7 +166,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
[[], null],
|
[[], null],
|
||||||
);
|
);
|
||||||
if (newRow === null) {
|
if (newRow === null) {
|
||||||
toast.error("Ошибка получения данных");
|
toast.error('Ошибка получения данных');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData((prevData) => {
|
setData((prevData) => {
|
||||||
@ -256,7 +179,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
const unsubscribeProgramAdds = subscribeToProgramAdds((msg) => {
|
const unsubscribeProgramAdds = subscribeToProgramAdds((msg) => {
|
||||||
let newRow = msg.result[1][0];
|
let newRow = msg.result[1][0];
|
||||||
setData((prevData) => {
|
setData((prevData) => {
|
||||||
const hasRoot = prevData.length > 0 && prevData[0].row_type === "ROOT";
|
const hasRoot = prevData.length > 0 && prevData[0].row_type === 'ROOT';
|
||||||
if (hasRoot) {
|
if (hasRoot) {
|
||||||
newRow = msg.result[1][1];
|
newRow = msg.result[1][1];
|
||||||
}
|
}
|
||||||
@ -291,7 +214,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
// Обработчик начала редактирования ячейки
|
// Обработчик начала редактирования ячейки
|
||||||
const unsubscribeCellEditStart = subscribeToCellEditStart((data) => {
|
const unsubscribeCellEditStart = subscribeToCellEditStart((data) => {
|
||||||
const dataCell = data.data;
|
const dataCell = data.data;
|
||||||
dataCell.column = "data." + dataCell.column;
|
dataCell.column = `data.${dataCell.column}`;
|
||||||
if (data.is_self) {
|
if (data.is_self) {
|
||||||
setEditingCells(dataCell);
|
setEditingCells(dataCell);
|
||||||
return;
|
return;
|
||||||
@ -318,9 +241,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribeErrors = subscribeToErrors((err) => {
|
const unsubscribeErrors = subscribeToErrors((err) => {
|
||||||
console.error("WebSocket error:", err);
|
console.error('WebSocket error:', err);
|
||||||
setError(err);
|
|
||||||
setTimeout(() => setError(null), 5000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -343,35 +264,12 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
subscribeToCellEditEnd,
|
subscribeToCellEditEnd,
|
||||||
subscribeToProgramAdds,
|
subscribeToProgramAdds,
|
||||||
subscribeToProjectAdds,
|
subscribeToProjectAdds,
|
||||||
formatedToSubRows,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateData = useCallback(
|
|
||||||
(newData) => {
|
|
||||||
if (Array.isArray(newData) && !newData[0]?.subRows) {
|
|
||||||
const maxDepth = newData.reduce(
|
|
||||||
(prev, current) => {
|
|
||||||
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
|
|
||||||
},
|
|
||||||
{ depth: 0 },
|
|
||||||
).depth;
|
|
||||||
|
|
||||||
const formattedData = formatedToSubRows(maxDepth, newData);
|
|
||||||
setData(formattedData);
|
|
||||||
} else {
|
|
||||||
setData(newData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[formatedToSubRows],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
setData,
|
|
||||||
columnsCurStage,
|
columnsCurStage,
|
||||||
isConnected,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
|
||||||
editingCells,
|
editingCells,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,32 +1,49 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import { validationRules } from '../constants/validation';
|
import { validationRules } from '../constants/validation';
|
||||||
import { buildColumnHeaderPaths, collectInvalidErrors, isValidationRuleEnabled } from '../utils/validationUtils';
|
import { buildColumnHeaderPaths, collectInvalidErrors, isValidationRuleEnabled } from '../utils/validationUtils';
|
||||||
|
|
||||||
export const useValidationRules = (data = [], columns = [], formType, sheetName) => {
|
const EMPTY_ARRAY = [];
|
||||||
const headerPaths = useMemo(() => buildColumnHeaderPaths(columns), [columns]);
|
|
||||||
|
export const useValidationRules = (data, columns, formType, sheetName) => {
|
||||||
|
const safeData = data || EMPTY_ARRAY;
|
||||||
|
const safeColumns = columns || EMPTY_ARRAY;
|
||||||
|
const stableErrorsRef = useRef([]);
|
||||||
|
const headerPaths = useMemo(() => buildColumnHeaderPaths(safeColumns), [safeColumns]);
|
||||||
const activeRules = useMemo(
|
const activeRules = useMemo(
|
||||||
() => validationRules.filter((rule) => isValidationRuleEnabled(rule, { formType, sheetName })),
|
() => validationRules.filter((rule) => isValidationRuleEnabled(rule, { formType, sheetName })),
|
||||||
[formType, sheetName],
|
[formType, sheetName],
|
||||||
);
|
);
|
||||||
|
|
||||||
const errors = useMemo(() => {
|
const calculatedErrors = useMemo(() => {
|
||||||
const result = [];
|
const result = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
for (const rule of activeRules) {
|
for (const rule of activeRules) {
|
||||||
collectInvalidErrors(data, rule, headerPaths, result, seen);
|
collectInvalidErrors(safeData, rule, headerPaths, result, seen);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [activeRules, data, headerPaths]);
|
}, [activeRules, headerPaths, safeData]);
|
||||||
|
|
||||||
|
const previousErrors = stableErrorsRef.current;
|
||||||
|
const errorsAreEqual =
|
||||||
|
previousErrors.length === calculatedErrors.length &&
|
||||||
|
previousErrors.every(
|
||||||
|
(error, index) =>
|
||||||
|
error.id === calculatedErrors[index].id &&
|
||||||
|
error.columnId === calculatedErrors[index].columnId &&
|
||||||
|
error.message === calculatedErrors[index].message,
|
||||||
|
);
|
||||||
|
if (!errorsAreEqual) {
|
||||||
|
stableErrorsRef.current = calculatedErrors;
|
||||||
|
}
|
||||||
|
const errors = stableErrorsRef.current;
|
||||||
|
|
||||||
const isCellInvalid = useCallback(
|
const isCellInvalid = useCallback(
|
||||||
(columnId, value, row) =>
|
(columnId, value, row) =>
|
||||||
activeRules.some(
|
activeRules.some(
|
||||||
(rule) =>
|
(rule) =>
|
||||||
rule.columnKeys.includes(columnId) &&
|
rule.columnKeys.includes(columnId) && (!rule.appliesToRow || rule.appliesToRow(row)) && rule.isInvalid(value, row, columnId),
|
||||||
(!rule.appliesToRow || rule.appliesToRow(row)) &&
|
|
||||||
rule.isInvalid(value, row, columnId),
|
|
||||||
),
|
),
|
||||||
[activeRules],
|
[activeRules],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,32 +1,19 @@
|
|||||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useRealtime } from './contexts/RealtimeContext';
|
import { useVspOptions } from './contexts/RealtimeContext';
|
||||||
|
|
||||||
import { sectionCodeColor } from './constants/columnConfig';
|
|
||||||
import { blueColumn, greenColumn } from './constants/columnColors';
|
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
|
||||||
import { ROLES_NAME_ID } from '../../constants/constants';
|
import { ROLES_NAME_ID } from '../../constants/constants';
|
||||||
|
import { blueColumn } from './constants/columnColors';
|
||||||
|
import { sectionCodeColor } from './constants/columnConfig';
|
||||||
|
|
||||||
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
||||||
|
|
||||||
const EditCellPortal = React.memo(({
|
const EditCellPortal = React.memo(({ cell, table, EditCell, onError, isEditable }) => {
|
||||||
cell,
|
|
||||||
table,
|
|
||||||
EditCell,
|
|
||||||
onSaveStart,
|
|
||||||
onSaveEnd,
|
|
||||||
onError,
|
|
||||||
isEditable
|
|
||||||
}) => {
|
|
||||||
const { formId, formType, sheetName, direction } = useParams();
|
const { formId, formType, sheetName, direction } = useParams();
|
||||||
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [refTbody, setRefTbody] = useState(null);
|
const [refTbody, setRefTbody] = useState(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
const { endEditing: contextEndEditing } = useRealtime();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
||||||
@ -36,54 +23,50 @@ const EditCellPortal = React.memo(({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = useCallback((val) => {
|
const handleChange = useCallback(
|
||||||
table.setEditingCell(null);
|
(val) => {
|
||||||
try {
|
table.setEditingCell(null);
|
||||||
if (table.options.meta?.updateCell) {
|
try {
|
||||||
const success = table.options.meta.updateCell(
|
if (table.options.meta?.updateCell) {
|
||||||
cell.row,
|
const success = table.options.meta.updateCell(cell.row, cell.column, val);
|
||||||
cell.column,
|
if (!success) {
|
||||||
val
|
console.error('Failed to update cell via WebSocket');
|
||||||
);
|
onError?.('Не удалось сохранить изменение');
|
||||||
if (!success) {
|
}
|
||||||
console.error('Failed to update cell via WebSocket');
|
|
||||||
onError?.('Не удалось сохранить изменение');
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating cell:', error);
|
||||||
|
onError?.(error.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.error('Error updating cell:', error);
|
[table, cell, onError],
|
||||||
onError?.(error.message);
|
);
|
||||||
}
|
|
||||||
}, [table, cell, onError]);
|
|
||||||
|
|
||||||
const editCellProps = useMemo(() => ({
|
const editCellProps = useMemo(
|
||||||
refCell: ref,
|
() => ({
|
||||||
cell,
|
refCell: ref,
|
||||||
value: cell.getValue(),
|
cell,
|
||||||
disabled: !isEditable,
|
value: cell.getValue(),
|
||||||
onChange: handleChange,
|
disabled: !isEditable,
|
||||||
tableId: tableKey,
|
onChange: handleChange,
|
||||||
table,
|
tableId: tableKey,
|
||||||
cellId: `${cell.row.id}_${cell.column.id}`,
|
table,
|
||||||
}), [cell, isEditable, handleChange, tableKey, table]);
|
cellId: `${cell.row.id}_${cell.column.id}`,
|
||||||
|
}),
|
||||||
|
[cell, isEditable, handleChange, tableKey, table],
|
||||||
|
);
|
||||||
|
|
||||||
const editType = cell.column.columnDef?.editType;
|
const editType = cell.column.columnDef?.editType;
|
||||||
|
|
||||||
const portalContent = useMemo(() => {
|
const portalContent = useMemo(() => {
|
||||||
if (!refTbody || isSaving) return null;
|
if (!refTbody) return null;
|
||||||
|
|
||||||
if (editType === 'vsp_dropdown') {
|
if (editType === 'vsp_dropdown') {
|
||||||
return createPortal(
|
return createPortal(<VspDropdownEditCell {...editCellProps} />, refTbody);
|
||||||
<VspDropdownEditCell {...editCellProps} />,
|
|
||||||
refTbody
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(<EditCell {...editCellProps} />, refTbody);
|
||||||
<EditCell {...editCellProps} />,
|
}, [refTbody, editCellProps, EditCell, editType]);
|
||||||
refTbody
|
|
||||||
);
|
|
||||||
}, [refTbody, isSaving, editCellProps, EditCell, editType]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -94,18 +77,41 @@ const EditCellPortal = React.memo(({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getColorCell(colors, column, row) {
|
function getColorCell(colors, column, row) {
|
||||||
if (row.original.data.section_code) {
|
const rowType = row?.original?.row_type || row?.row_type;
|
||||||
const section_code = row.original.data.section_code[0];
|
const sectionCodeValue = row?.original?.data?.section_code ?? row?.original?.data?.header?.section_code;
|
||||||
return (sectionCodeColor[section_code] || blueColumn)[row.original?.row_type || row.row_type];
|
|
||||||
|
if (sectionCodeValue !== null && sectionCodeValue !== undefined && sectionCodeValue !== '') {
|
||||||
|
const sectionCode = String(sectionCodeValue)[0];
|
||||||
|
return (sectionCodeColor[sectionCode] || blueColumn)[rowType];
|
||||||
}
|
}
|
||||||
return colors[column.id]?.color_type?.[row.original?.row_type || row.row_type];
|
|
||||||
|
return colors[column.id]?.color_type?.[rowType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UPPERCASE_MONTH_PATTERN = /ЯНВАРЬ|ФЕВРАЛЬ|МАРТ|АПРЕЛЬ|МАЙ|ИЮНЬ|ИЮЛЬ|АВГУСТ|СЕНТЯБРЬ|ОКТЯБРЬ|НОЯБРЬ|ДЕКАБРЬ/g;
|
||||||
|
|
||||||
|
const normalizeMonthHeader = (header) => {
|
||||||
|
if (typeof header !== 'string') return header;
|
||||||
|
|
||||||
|
return header.replace(UPPERCASE_MONTH_PATTERN, (month, offset) => {
|
||||||
|
const normalizedMonth = month.toLowerCase();
|
||||||
|
if (offset !== 0) return normalizedMonth;
|
||||||
|
|
||||||
|
return normalizedMonth[0].toUpperCase() + normalizedMonth.slice(1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const CellWithVspOptions = React.memo(({ CellComponent, cellProps }) => {
|
||||||
|
const vspOptions = useVspOptions();
|
||||||
|
return <CellComponent {...cellProps} vspOptions={vspOptions} />;
|
||||||
|
});
|
||||||
|
|
||||||
export const getTableColumns = ({
|
export const getTableColumns = ({
|
||||||
Cell,
|
Cell,
|
||||||
EditCell,
|
EditCell,
|
||||||
columnsConfig,
|
columnsConfig,
|
||||||
columnsCurStage,
|
columnsCurStage,
|
||||||
|
userRoleId,
|
||||||
onCellUpdateError,
|
onCellUpdateError,
|
||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
isCellInvalid,
|
isCellInvalid,
|
||||||
@ -113,24 +119,23 @@ export const getTableColumns = ({
|
|||||||
const columnColors = { ...columnsConfig.colors };
|
const columnColors = { ...columnsConfig.colors };
|
||||||
const columns = structuredClone(columnsConfig.columns);
|
const columns = structuredClone(columnsConfig.columns);
|
||||||
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
const cellPropsCache = new WeakMap();
|
const cellPropsCache = new WeakMap();
|
||||||
const editPropsCache = new WeakMap();
|
const editPropsCache = new WeakMap();
|
||||||
|
|
||||||
const isEditable = (row, column) => {
|
const isEditable = (row, column) => {
|
||||||
if (row.original?.row_type !== 'INPUT') return false;
|
if (row.original?.row_type !== 'INPUT') return false;
|
||||||
if (user.role_id === ROLES_NAME_ID.admin) return true;
|
if (userRoleId === ROLES_NAME_ID.admin) return true;
|
||||||
const keyCol = column.id;
|
const keyCol = column.id;
|
||||||
if (columnsCurStage.includes(keyCol)) return true;
|
if (columnsCurStage.includes(keyCol)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getCachedCellProps = (row, column, table) => {
|
const getCachedCellProps = (row, column, table) => {
|
||||||
const key = `${row.id}_${column.id}`;
|
const key = `${row.id}_${column.id}`;
|
||||||
const value = row.getValue(column.id);
|
const value = row.getValue(column.id);
|
||||||
const isInvalid = isCellInvalid?.(column.id, value, row.original) || false;
|
const isInvalid = isCellInvalid?.(column.id, value, row.original) || false;
|
||||||
|
const isEditableCell = isEditable(row, column);
|
||||||
|
const backgroundColor = getColorCell(columnColors, column, row);
|
||||||
if (!cellPropsCache.has(row)) {
|
if (!cellPropsCache.has(row)) {
|
||||||
cellPropsCache.set(row, {});
|
cellPropsCache.set(row, {});
|
||||||
}
|
}
|
||||||
@ -139,35 +144,37 @@ export const getTableColumns = ({
|
|||||||
if (rowCache[key]) {
|
if (rowCache[key]) {
|
||||||
const cached = rowCache[key];
|
const cached = rowCache[key];
|
||||||
const currentGlobalFilter = table.getState().globalFilter;
|
const currentGlobalFilter = table.getState().globalFilter;
|
||||||
const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
const currentColumnFilter = table.getState().columnFilters?.find((f) => f.id === column.id)?.value;
|
||||||
|
|
||||||
if (cached.globalFilter === currentGlobalFilter &&
|
if (
|
||||||
|
cached.globalFilter === currentGlobalFilter &&
|
||||||
cached.columnFilter === currentColumnFilter &&
|
cached.columnFilter === currentColumnFilter &&
|
||||||
cached.value === value &&
|
cached.value === value &&
|
||||||
cached.isInvalid === isInvalid) {
|
cached.isInvalid === isInvalid &&
|
||||||
|
cached.backgroundColor === backgroundColor
|
||||||
|
) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalFilter = table.getState().globalFilter;
|
const globalFilter = table.getState().globalFilter;
|
||||||
const columnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
const columnFilter = table.getState().columnFilters?.find((f) => f.id === column.id)?.value;
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
globalFilter,
|
globalFilter,
|
||||||
columnFilter,
|
columnFilter,
|
||||||
isEditable: isEditable(row, column),
|
isEditable: isEditableCell,
|
||||||
backgroundColor: getColorCell(columnColors, column, row),
|
backgroundColor,
|
||||||
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
|
|
||||||
isInvalid,
|
isInvalid,
|
||||||
value,
|
value,
|
||||||
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}_${value}_${isInvalid}`,
|
_hash: `${globalFilter}_${columnFilter}_${key}_${value}_${isInvalid}_${backgroundColor}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
rowCache[key] = props;
|
rowCache[key] = props;
|
||||||
return props;
|
return props;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCachedEditProps = (row, column, table) => {
|
const getCachedEditProps = (row, column) => {
|
||||||
const key = `${row.id}_${column.id}`;
|
const key = `${row.id}_${column.id}`;
|
||||||
|
|
||||||
if (!editPropsCache.has(row)) {
|
if (!editPropsCache.has(row)) {
|
||||||
@ -190,33 +197,32 @@ export const getTableColumns = ({
|
|||||||
// Оптимизированная функция processColumns
|
// Оптимизированная функция processColumns
|
||||||
const processColumns = (columns) => {
|
const processColumns = (columns) => {
|
||||||
for (const col of columns) {
|
for (const col of columns) {
|
||||||
|
col.header = normalizeMonthHeader(col.header);
|
||||||
|
const usesVspOptions = col.editType === 'vsp_dropdown';
|
||||||
|
|
||||||
col.Cell = ({ cell, table, column, row }) => {
|
col.Cell = ({ cell, table, column, row }) => {
|
||||||
const { vspOptions } = useRealtime();
|
|
||||||
const props = getCachedCellProps(row, column, table);
|
const props = getCachedCellProps(row, column, table);
|
||||||
|
|
||||||
const cellProps = useMemo(() => ({
|
const cellProps = useMemo(
|
||||||
row,
|
() => ({
|
||||||
column,
|
row,
|
||||||
cell,
|
column,
|
||||||
vspOptions,
|
cell,
|
||||||
...props,
|
...props,
|
||||||
}), [row, column, cell, props, vspOptions,]);
|
}),
|
||||||
|
[row, column, cell, props],
|
||||||
|
);
|
||||||
|
|
||||||
return <Cell {...cellProps} />;
|
return usesVspOptions ? <CellWithVspOptions CellComponent={Cell} cellProps={cellProps} /> : <Cell {...cellProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
col.Edit = ({ cell, table, row, column }) => {
|
col.Edit = ({ cell, table, row, column }) => {
|
||||||
const props = getCachedEditProps(row, column, table);
|
const props = getCachedEditProps(row, column);
|
||||||
|
|
||||||
const editComponent = useMemo(() => (
|
const editComponent = useMemo(
|
||||||
<EditCellPortal
|
() => <EditCellPortal cell={cell} table={table} EditCell={EditCell} onError={onCellUpdateError} isEditable={props.isEditable} />,
|
||||||
cell={cell}
|
[cell, table, EditCell, onCellUpdateError, props.isEditable],
|
||||||
table={table}
|
);
|
||||||
EditCell={EditCell}
|
|
||||||
onError={onCellUpdateError}
|
|
||||||
isEditable={props.isEditable}
|
|
||||||
/>
|
|
||||||
), [cell, table, EditCell, onCellUpdateError, props.isEditable]);
|
|
||||||
|
|
||||||
return editComponent;
|
return editComponent;
|
||||||
};
|
};
|
||||||
@ -229,23 +235,22 @@ export const getTableColumns = ({
|
|||||||
processColumns(col.columns);
|
processColumns(col.columns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
processColumns(columns);
|
processColumns(columns);
|
||||||
|
|
||||||
const rowNumber = {
|
const rowNumber = {
|
||||||
accessorKey: 'sort_order',
|
accessorKey: 'sort_order',
|
||||||
header: '#',
|
header: '№',
|
||||||
size: 50,
|
size: 50,
|
||||||
enableEditing: false,
|
enableEditing: false,
|
||||||
Cell: ({ cell, row }) => {
|
Cell: ({ cell, row }) => {
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onCellNumberClick(row.id);
|
onCellNumberClick(row.id);
|
||||||
}, [row.id, onCellNumberClick]);
|
}, [row.id, onCellNumberClick]);
|
||||||
return <button type="button" onClick={handleClick}>{`${cell.getValue()} `}</button>;
|
return <button type='button' onClick={handleClick}>{`${cell.getValue()} `}</button>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return [rowNumber, ...columns];
|
return [rowNumber, ...columns];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,35 +2,38 @@ import { ArrowDropDown, ChevronRight, ExpandMore } from '@mui/icons-material';
|
|||||||
import { CheckBox, CheckBoxOutlineBlank, IndeterminateCheckBox } from '@mui/icons-material';
|
import { CheckBox, CheckBoxOutlineBlank, IndeterminateCheckBox } from '@mui/icons-material';
|
||||||
import { Box, Checkbox, InputAdornment, Popover, TextField, Typography } from '@mui/material';
|
import { Box, Checkbox, InputAdornment, Popover, TextField, Typography } from '@mui/material';
|
||||||
import { SimpleTreeView, TreeItem } from '@mui/x-tree-view';
|
import { SimpleTreeView, TreeItem } from '@mui/x-tree-view';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { memo, useMemo, useState } from 'react';
|
||||||
import { filterColumnTree, getColumnTreeLeafKeys } from '../RealtimeTable/utils/columnUtils';
|
import { filterColumnTree, getColumnTreeLeafKeys } from '../RealtimeTable/utils/columnUtils';
|
||||||
|
|
||||||
|
const EMPTY_IDS = [];
|
||||||
|
|
||||||
const CollapsibleTree = ({
|
const CollapsibleTree = ({
|
||||||
columnTree,
|
columnTree,
|
||||||
excludeColumnKeys = [],
|
excludeColumnKeys = EMPTY_IDS,
|
||||||
placeholder = 'Скрыть столбцы',
|
placeholder = 'Скрыть столбцы',
|
||||||
placeholderSelect = 'Скрыто столбцов: ',
|
placeholderSelect = 'Скрыто столбцов: ',
|
||||||
selectedIds = [],
|
selectedIds = EMPTY_IDS,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
|
onSelectAllNodes,
|
||||||
forStage = false,
|
forStage = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedItems, setExpandedItems] = useState([]);
|
const [expandedItems, setExpandedItems] = useState([]);
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const [selectedNodes, setSelectedNodes] = useState([]);
|
const selectedNodes = selectedIds;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedNodes(selectedIds);
|
|
||||||
}, [selectedIds]);
|
|
||||||
|
|
||||||
const visibleColumnTree = useMemo(() => filterColumnTree(columnTree, excludeColumnKeys), [columnTree, excludeColumnKeys]);
|
const visibleColumnTree = useMemo(() => filterColumnTree(columnTree, excludeColumnKeys), [columnTree, excludeColumnKeys]);
|
||||||
|
|
||||||
const visibleLeafKeys = useMemo(() => getColumnTreeLeafKeys(visibleColumnTree), [visibleColumnTree]);
|
const visibleLeafKeys = useMemo(() => getColumnTreeLeafKeys(visibleColumnTree), [visibleColumnTree]);
|
||||||
|
|
||||||
const handleExpandedItemsChange = (event, itemIds) => {
|
const handleExpandedItemsChange = (_event, itemIds) => {
|
||||||
setExpandedItems(itemIds);
|
setExpandedItems(itemIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = (isSelected) => {
|
const handleSelectAll = (isSelected) => {
|
||||||
|
if (onSelectAllNodes) {
|
||||||
|
onSelectAllNodes(visibleLeafKeys, !isSelected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const col of visibleColumnTree) {
|
for (const col of visibleColumnTree) {
|
||||||
onSelectNode?.(col, !isSelected);
|
onSelectNode?.(col, !isSelected);
|
||||||
}
|
}
|
||||||
@ -248,4 +251,4 @@ const CollapsibleTree = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CollapsibleTree;
|
export default memo(CollapsibleTree);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { InputAdornment, TextField } from '@mui/material';
|
import { InputAdornment, TextField } from '@mui/material';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
import { Search } from './icons/icons';
|
import { Search } from './icons/icons';
|
||||||
|
|
||||||
function SearchComponent({ placeholder = 'Поиск...', value = '', onChange, debounceMs = 0, height = '2rem', sx: sxProp, ...rest }) {
|
function SearchComponent({ placeholder = 'Поиск...', value = '', onChange, debounceMs = 0, height = '2rem', sx: sxProp, ...rest }) {
|
||||||
@ -75,4 +75,4 @@ function SearchComponent({ placeholder = 'Поиск...', value = '', onChange,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchComponent;
|
export default memo(SearchComponent);
|
||||||
|
|||||||
@ -281,6 +281,9 @@ const UserTableForAdd = ({ usersEmail = [], sspOptions = [], onDeleteUser, onCha
|
|||||||
maxHeight: '18.38rem',
|
maxHeight: '18.38rem',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
localization: {
|
||||||
|
noRecordsToDisplay: 'Нет данных для отображения',
|
||||||
|
},
|
||||||
enableRowNumbers: false,
|
enableRowNumbers: false,
|
||||||
muiToolbarAlertBannerProps: false,
|
muiToolbarAlertBannerProps: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -472,6 +472,13 @@ const SummaryPage = () => {
|
|||||||
columns: firstColumns,
|
columns: firstColumns,
|
||||||
data: filteredData,
|
data: filteredData,
|
||||||
...projectsTableOptions,
|
...projectsTableOptions,
|
||||||
|
localization: {
|
||||||
|
expand: 'Раскрыть',
|
||||||
|
expandAll: 'Раскрыть все',
|
||||||
|
collapse: 'Свернуть',
|
||||||
|
collapseAll: 'Свернуть все',
|
||||||
|
noRecordsToDisplay: 'Нет данных для отображения',
|
||||||
|
},
|
||||||
initialState: {
|
initialState: {
|
||||||
columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] },
|
columnPinning: { right: ['actions'], left: ['mrt-row-expand', 'project'] },
|
||||||
},
|
},
|
||||||
@ -482,6 +489,9 @@ const SummaryPage = () => {
|
|||||||
columns: secondColumns,
|
columns: secondColumns,
|
||||||
data: summaryData,
|
data: summaryData,
|
||||||
...summaryProjectTableOptions,
|
...summaryProjectTableOptions,
|
||||||
|
localization: {
|
||||||
|
noRecordsToDisplay: 'Нет данных для отображения',
|
||||||
|
},
|
||||||
initialState: {
|
initialState: {
|
||||||
columnPinning: { left: ['data.header.name'] },
|
columnPinning: { left: ['data.header.name'] },
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user