Модалки в DictVspModals (forwardRef), таблица в memo со стабильными колбэками, enableRowVirtualization, size/minSize/maxSize колонок выверены под grid-раскладку. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
456 lines
12 KiB
JavaScript
456 lines
12 KiB
JavaScript
import { Box, IconButton, Stack, Typography } from '@mui/material';
|
|
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
|
import { memo, useMemo, useRef } from 'react';
|
|
import { useAuth } from '../../../app/context/AuthProvider';
|
|
import { PrimaryButton, PrimaryOutlinedButton } from '../../../components/common/Buttons/Buttons';
|
|
import { EditPenSvg } from '../../../components/common/icons/icons';
|
|
import { ROLES_NAME_ID } from '../../../constants/constants';
|
|
import { filterVspBySsp } from '../utils/filterVspBySsp';
|
|
|
|
const ROW_ESTIMATED_HEIGHT = 54;
|
|
|
|
const TableDictVsp = ({ data, onEditClick, onAddClick, onExportClick, loading = false, selectedSsp }) => {
|
|
const { user } = useAuth();
|
|
const tableContainerRef = useRef(null);
|
|
|
|
const filteredData = useMemo(() => filterVspBySsp(data, selectedSsp), [data, selectedSsp]);
|
|
|
|
const columns = useMemo(
|
|
() => [
|
|
{
|
|
accessorKey: 'index',
|
|
header: '№',
|
|
size: 60,
|
|
minSize: 50,
|
|
maxSize: 70,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
Cell: ({ row }) => (
|
|
<Typography variant='body2' component='div' sx={{ fontWeight: 'medium' }}>
|
|
{row.index + 1}
|
|
</Typography>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'system_code',
|
|
header: 'Код РФ',
|
|
size: 110,
|
|
minSize: 90,
|
|
maxSize: 140,
|
|
Cell: ({ cell }) => (
|
|
<Typography variant='body2' component='div' sx={{ fontWeight: 'medium' }}>
|
|
{cell.getValue() || '-'}
|
|
</Typography>
|
|
),
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'registration_number',
|
|
header: 'Регистрационный номер ВСП',
|
|
size: 250,
|
|
minSize: 200,
|
|
maxSize: 300,
|
|
Cell: ({ cell }) => (
|
|
<Typography variant='body2' component='div' sx={{ fontWeight: 'medium' }}>
|
|
{cell.getValue() || '-'}
|
|
</Typography>
|
|
),
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'regional_branch',
|
|
header: 'Региональный филиал/ССП',
|
|
size: 230,
|
|
minSize: 180,
|
|
maxSize: 300,
|
|
Cell: ({ cell }) => <Typography variant='body2'>{cell.getValue() || '-'}</Typography>,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'vsp_type',
|
|
header: 'Тип ВСП',
|
|
size: 120,
|
|
minSize: 100,
|
|
maxSize: 150,
|
|
Cell: ({ cell }) => <Typography variant='body2'>{cell.getValue() || '-'}</Typography>,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'address',
|
|
header: 'Адрес объекта',
|
|
size: 300,
|
|
minSize: 200,
|
|
maxSize: 500,
|
|
Cell: ({ cell }) => <Typography variant='body2'>{cell.getValue() || '-'}</Typography>,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'approved_format',
|
|
header: 'Утверждённый формат',
|
|
size: 190,
|
|
minSize: 160,
|
|
maxSize: 240,
|
|
Cell: ({ cell }) => <Typography variant='body2'>{cell.getValue() || '-'}</Typography>,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'open_date',
|
|
header: 'Дата открытия ВСП',
|
|
size: 180,
|
|
minSize: 150,
|
|
maxSize: 220,
|
|
Cell: ({ cell }) => {
|
|
const date = cell.getValue();
|
|
return <Typography variant='body2'>{date ? new Date(date).toLocaleDateString('ru-RU') : '-'}</Typography>;
|
|
},
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'close_date',
|
|
header: 'Дата закрытия ВСП',
|
|
size: 180,
|
|
minSize: 150,
|
|
maxSize: 220,
|
|
Cell: ({ cell }) => {
|
|
const date = cell.getValue();
|
|
return <Typography variant='body2'>{date ? new Date(date).toLocaleDateString('ru-RU') : '-'}</Typography>;
|
|
},
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'placement_type',
|
|
header: 'Форма расположения ВСП',
|
|
size: 220,
|
|
minSize: 180,
|
|
maxSize: 260,
|
|
Cell: ({ cell }) => <Typography variant='body2'>{cell.getValue() || '-'}</Typography>,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'staff_count',
|
|
header: 'Штатная численность ВСП',
|
|
size: 230,
|
|
minSize: 190,
|
|
maxSize: 270,
|
|
Cell: ({ cell }) => {
|
|
const value = cell.getValue();
|
|
return <Typography variant='body2'>{value !== undefined && value !== null ? value : '-'}</Typography>;
|
|
},
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'area',
|
|
header: 'Площадь',
|
|
size: 100,
|
|
minSize: 80,
|
|
maxSize: 120,
|
|
Cell: ({ cell }) => <Typography variant='body2'>{cell.getValue() || '-'}</Typography>,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'rent_contract_num',
|
|
header: 'Номер договора',
|
|
size: 150,
|
|
minSize: 130,
|
|
maxSize: 200,
|
|
Cell: ({ cell }) => <Typography variant='body2'>{cell.getValue() || '-'}</Typography>,
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'rent_end_date',
|
|
header: 'Дата окончания договора',
|
|
size: 230,
|
|
minSize: 190,
|
|
maxSize: 270,
|
|
Cell: ({ cell }) => {
|
|
const date = cell.getValue();
|
|
return <Typography variant='body2'>{date ? new Date(date).toLocaleDateString('ru-RU') : '-'}</Typography>;
|
|
},
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: 'notes',
|
|
header: 'Примечания',
|
|
size: 300,
|
|
minSize: 200,
|
|
maxSize: 500,
|
|
Cell: ({ cell }) => (
|
|
<Typography variant='body2' sx={{ whiteSpace: 'pre-wrap' }}>
|
|
{cell.getValue() || '-'}
|
|
</Typography>
|
|
),
|
|
enableColumnActions: false,
|
|
enableSorting: false,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
const table = useMaterialReactTable({
|
|
columns,
|
|
data: filteredData,
|
|
enableEditing: true,
|
|
enableRowActions: true,
|
|
enableTopToolbar: true,
|
|
enableColumnFilters: false,
|
|
enableGlobalFilter: false,
|
|
enableSorting: false,
|
|
enablePagination: false,
|
|
enableRowVirtualization: true,
|
|
// Высоту строк MRT измеряет сам (по умолчанию): «Примечания» многострочные (pre-wrap).
|
|
rowVirtualizerOptions: { overscan: 5, estimateSize: () => ROW_ESTIMATED_HEIGHT },
|
|
positionActionsColumn: 'last',
|
|
|
|
// Включаем изменение размера колонок для лучшего UX при горизонтальном скролле
|
|
// enableColumnResizing: true,
|
|
columnResizeMode: 'onChange',
|
|
|
|
enableToolbarInternalActions: false,
|
|
|
|
initialState: {
|
|
columnPinning: {
|
|
left: ['index', 'system_code', 'registration_number'],
|
|
right: ['mrt-row-actions'],
|
|
},
|
|
},
|
|
|
|
renderTopToolbar: () => (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
}}>
|
|
<Typography
|
|
sx={{
|
|
fontWeight: 700,
|
|
fontSize: '1.25rem',
|
|
lineHeight: '140%',
|
|
color: '#2d3748',
|
|
}}>
|
|
Справочник объектов
|
|
</Typography>
|
|
<Stack spacing={'.5rem'} direction={'row'}>
|
|
<PrimaryOutlinedButton onClick={onExportClick}>Экспортировать</PrimaryOutlinedButton>
|
|
{user.role_id == ROLES_NAME_ID.admin && <PrimaryButton onClick={onAddClick}>+ Добавить объект</PrimaryButton>}
|
|
</Stack>
|
|
</Box>
|
|
),
|
|
|
|
muiTopToolbarProps: {
|
|
sx: {
|
|
backgroundColor: '#F9FAFB',
|
|
minHeight: 'auto',
|
|
padding: '1rem 1rem 0 1rem',
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 10,
|
|
'& .MuiToolbar-root': {
|
|
padding: 0,
|
|
minHeight: 'auto',
|
|
width: '100%',
|
|
},
|
|
},
|
|
},
|
|
|
|
displayColumnDefOptions: {
|
|
'mrt-row-actions': {
|
|
header: 'Действия',
|
|
size: 80,
|
|
muiTableHeadCellProps: {
|
|
align: 'center',
|
|
},
|
|
muiTableBodyCellProps: {
|
|
align: 'center',
|
|
},
|
|
},
|
|
},
|
|
|
|
renderRowActions: ({ row }) => (
|
|
<>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 1,
|
|
}}>
|
|
<IconButton
|
|
size='small'
|
|
onClick={() => onEditClick(row.original)}
|
|
sx={{
|
|
padding: '4px',
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
|
},
|
|
}}
|
|
title='Редактировать'>
|
|
<EditPenSvg fill='#6FB246' />
|
|
</IconButton>
|
|
</Box>
|
|
</>
|
|
),
|
|
|
|
state: {
|
|
isLoading: loading,
|
|
},
|
|
|
|
muiTablePaperProps: {
|
|
sx: {
|
|
borderRadius: '.63rem',
|
|
border: '1px solid #f2f2f2',
|
|
boxShadow: 'none',
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
padding: '20px',
|
|
width: '100%',
|
|
},
|
|
},
|
|
|
|
muiTableContainerProps: {
|
|
ref: tableContainerRef,
|
|
sx: {
|
|
maxHeight: 'calc(100vh - 450px)',
|
|
minHeight: '40rem',
|
|
width: '100%',
|
|
marginTop: '1.5rem',
|
|
overflowX: 'auto', // Включаем горизонтальный скролл
|
|
overflowY: 'auto', // Вертикальный скролл
|
|
position: 'relative',
|
|
// Добавляем индикатор скролла для лучшего UX
|
|
'&::-webkit-scrollbar': {
|
|
height: '8px',
|
|
width: '8px',
|
|
},
|
|
'&::-webkit-scrollbar-track': {
|
|
backgroundColor: '#f1f1f1',
|
|
borderRadius: '4px',
|
|
},
|
|
'&::-webkit-scrollbar-thumb': {
|
|
backgroundColor: '#c1c1c1',
|
|
borderRadius: '4px',
|
|
'&:hover': {
|
|
backgroundColor: '#a8a8a8',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
// Обеспечиваем минимальную ширину таблицы для скролла
|
|
muiTableProps: {
|
|
sx: {
|
|
minWidth: 'max-content', // Таблица будет занимать минимум необходимую ширину
|
|
width: '100%',
|
|
},
|
|
},
|
|
|
|
muiTableBodyRowProps: {
|
|
sx: {
|
|
'& .MuiTableCell-root': {
|
|
padding: '0.75rem',
|
|
},
|
|
'&:hover': {
|
|
backgroundColor: 'white !important',
|
|
},
|
|
'&:hover td::after': {
|
|
opacity: '0 !important',
|
|
backgroundColor: 'transparent !important',
|
|
},
|
|
'&:hover .MuiTableCell-root': {
|
|
backgroundColor: 'white !important',
|
|
},
|
|
},
|
|
},
|
|
|
|
muiTableHeadProps: {
|
|
sx: {
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 10,
|
|
},
|
|
},
|
|
|
|
muiTableHeadRowProps: {
|
|
sx: {
|
|
position: 'sticky',
|
|
backgroundColor: '#F9FAFB',
|
|
color: '#4A5565',
|
|
fontSize: '0.88rem',
|
|
'& .MuiTableCell-root': {
|
|
padding: '1rem 0.75rem',
|
|
fontWeight: 'bold',
|
|
backgroundColor: '#F9FAFB',
|
|
whiteSpace: 'nowrap', // Заголовки тоже не переносим
|
|
},
|
|
'& .Mui-TableHeadCell-Content': {
|
|
'& .Mui-TableHeadCell-Content-Labels': {
|
|
overflow: 'visible',
|
|
},
|
|
'& .MuiSvgIcon-root': {
|
|
fontSize: '1.2rem',
|
|
marginLeft: '8px',
|
|
overflow: 'visible !important',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
muiTableBodyCellProps: {
|
|
sx: {
|
|
padding: '0.75rem',
|
|
fontSize: '0.75rem',
|
|
lineHeight: '150%',
|
|
color: '#364153',
|
|
whiteSpace: 'nowrap', // Предотвращаем перенос текста
|
|
},
|
|
},
|
|
|
|
muiTableHeadCellProps: {
|
|
sx: {
|
|
padding: '1rem 0.75rem',
|
|
fontSize: 'inherit',
|
|
fontWeight: 'bold',
|
|
whiteSpace: 'nowrap', // Заголовки не переносим
|
|
'& .MuiSvgIcon-root': {
|
|
fontSize: '1.2rem',
|
|
marginLeft: '4px',
|
|
flexShrink: 0,
|
|
},
|
|
},
|
|
},
|
|
|
|
muiBottomToolbarProps: {
|
|
sx: {
|
|
display: 'none',
|
|
},
|
|
},
|
|
|
|
localization: {
|
|
noRecordsToDisplay: 'Нет данных для отображения',
|
|
},
|
|
});
|
|
|
|
return (
|
|
<Box sx={{ position: 'relative', width: '100%' }}>
|
|
<MaterialReactTable table={table} />
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default memo(TableDictVsp);
|