257 lines
8.8 KiB
JavaScript
257 lines
8.8 KiB
JavaScript
// src/components/common/PaginatedList/PaginatedList.jsx
|
||
import { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
Pagination,
|
||
PaginationItem,
|
||
Select,
|
||
MenuItem,
|
||
Typography,
|
||
Stack,
|
||
} from '@mui/material';
|
||
import { CardsSkeleton } from './Skeleton';
|
||
import {
|
||
ExportExitButton,
|
||
ExportWithTextButton,
|
||
} from './Buttons/ButtonsActions';
|
||
import { exportBulkForms, exportSingleForm } from '../../utils/exportFile';
|
||
|
||
/**
|
||
* Универсальный компонент для отображения списка с пагинацией и экспортом
|
||
*
|
||
* @param {Object} props
|
||
* @param {Array} props.items - Массив элементов для отображения
|
||
* @param {boolean} props.loading - Состояние загрузки
|
||
* @param {number} props.totalCount - Общее количество элементов
|
||
* @param {number} props.page - Текущая страница
|
||
* @param {number} props.limit - Количество элементов на странице
|
||
* @param {Function} props.onPageChange - Обработчик изменения страницы
|
||
* @param {Function} props.onLimitChange - Обработчик изменения лимита
|
||
* @param {Function} props.renderItem - Функция рендеринга элемента
|
||
* @param {Function} props.getItemId - Функция получения ID элемента
|
||
* @param {Function} props.getItemTitle - Функция получения заголовка элемента
|
||
* @param {string} props.entityName - Название сущности (для текстов)
|
||
* @param {string} props.emptyMessage - Сообщение при пустом списке
|
||
* @param {number} props.skeletonCount - Количество силуэтов таблиц при загрузки
|
||
* @param {Object} props.extraActions - Дополнительные действия справа
|
||
* @param {boolean} props.enableExport - Включить экспорт
|
||
* @param {Array} props.limitOptions - Опции для выбора лимита
|
||
* @param {number} props.maxHeight - Максимальная высота контейнера со скроллом
|
||
* @param {boolean} props.enableScroll - Включить скролл для списка
|
||
* @param {boolean} props.filterActions - Элементы фильтрации
|
||
*/
|
||
export function PaginatedList({
|
||
items = [],
|
||
loading = false,
|
||
totalCount = 0,
|
||
page = 1,
|
||
limit = 20,
|
||
onPageChange,
|
||
onLimitChange,
|
||
renderItem,
|
||
getItemId,
|
||
getItemTitle = (item) => `Элемент ${getItemId(item)}`,
|
||
entityName = 'элементов',
|
||
emptyMessage = 'Элементы не найдены',
|
||
skeletonCount = 20,
|
||
extraActions = null,
|
||
enableExport = true,
|
||
limitOptions = [10, 20, 50, 100, 200],
|
||
maxHeight = 'calc(100vh - 300px)',
|
||
enableScroll = true,
|
||
filterActions = null,
|
||
}) {
|
||
const [exportMode, setExportMode] = useState(false);
|
||
const [exportList, setExportList] = useState([]);
|
||
|
||
// Сброс списка экспорта при переключении режима
|
||
useEffect(() => {
|
||
setExportList([]);
|
||
}, [exportMode]);
|
||
|
||
const handleAddToExport = (itemId, checked) => {
|
||
if (checked) {
|
||
setExportList((prev) => [...prev, itemId]);
|
||
} else {
|
||
setExportList((prev) => prev.filter((id) => id !== itemId));
|
||
}
|
||
};
|
||
|
||
const handleBulkExport = async () => {
|
||
await exportBulkForms(exportList);
|
||
setExportMode(false);
|
||
};
|
||
|
||
const handleSingleExport = (itemId) => {
|
||
const item = items.find((item) => getItemId(item) === itemId);
|
||
const title = item ? getItemTitle(item) : `Элемент ${itemId}`;
|
||
exportSingleForm(itemId, title);
|
||
};
|
||
|
||
// Если включен экспорт, передаем его пропсы в дочерние элементы
|
||
const renderItemWithExport = (item) => {
|
||
if (enableExport) {
|
||
return renderItem(item, {
|
||
exportMode,
|
||
onAddForExport: (checked) => {
|
||
handleAddToExport(getItemId(item), checked);
|
||
},
|
||
onExportClick: () => {
|
||
handleSingleExport(getItemId(item));
|
||
},
|
||
isSelected: exportList.includes(getItemId(item)),
|
||
});
|
||
}
|
||
return renderItem(item);
|
||
};
|
||
|
||
return (
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
|
||
{/* Верхняя панель с экспортом и дополнительными действиями */}
|
||
{enableExport && (
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||
<div>
|
||
{filterActions}
|
||
</div>
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: '1.5rem' }}>
|
||
{exportMode && (
|
||
<Typography variant="body2" color="textSecondary">
|
||
Выделено {entityName}: {exportList.length}
|
||
</Typography>
|
||
)}
|
||
<Box sx={{ flex: 1 }} />
|
||
<Stack direction="row" spacing="0.5rem">
|
||
{!exportMode ? (
|
||
<>
|
||
<ExportWithTextButton
|
||
text="Пакетный экспорт"
|
||
onClick={() => setExportMode(true)}
|
||
height='2.5rem'
|
||
/>
|
||
{extraActions}
|
||
</>
|
||
) : (
|
||
<>
|
||
<ExportWithTextButton
|
||
text="Экспортировать"
|
||
onClick={handleBulkExport}
|
||
disabled={exportList.length === 0}
|
||
height='2.5rem'
|
||
/>
|
||
<ExportExitButton onClick={() => setExportMode(false)} />
|
||
</>
|
||
)}
|
||
</Stack>
|
||
</Box>
|
||
</Box>
|
||
|
||
)}
|
||
|
||
{/* Контейнер со скроллом для списка */}
|
||
<Box
|
||
sx={{
|
||
flex: 1,
|
||
overflow: enableScroll ? 'auto' : 'visible',
|
||
maxHeight: enableScroll ? maxHeight : 'none',
|
||
'&::-webkit-scrollbar': {
|
||
width: '8px',
|
||
},
|
||
'&::-webkit-scrollbar-track': {
|
||
background: '#f1f1f1',
|
||
borderRadius: '4px',
|
||
},
|
||
'&::-webkit-scrollbar-thumb': {
|
||
background: '#888',
|
||
borderRadius: '4px',
|
||
},
|
||
'&::-webkit-scrollbar-thumb:hover': {
|
||
background: '#555',
|
||
},
|
||
}}
|
||
>
|
||
{/* Список элементов */}
|
||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||
{loading ? (
|
||
<CardsSkeleton count={skeletonCount} />
|
||
) : items.length === 0 ? (
|
||
<Box sx={{ textAlign: 'center', py: 4, width: '100%' }}>
|
||
<Typography variant="body1" color="textSecondary">
|
||
{emptyMessage}
|
||
</Typography>
|
||
</Box>
|
||
) : (
|
||
items.map((item) => renderItemWithExport(item))
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Пагинация */}
|
||
{!loading && items.length > 0 && (
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
pt: 2,
|
||
pb: 2,
|
||
borderTop: '1px solid',
|
||
borderColor: 'divider',
|
||
flexShrink: 0,
|
||
mt: 2,
|
||
}}
|
||
>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
<Typography variant="body2" color="textSecondary">
|
||
Показать на странице:
|
||
</Typography>
|
||
<Select
|
||
value={limit}
|
||
onChange={onLimitChange}
|
||
size="small"
|
||
sx={{ minWidth: 70 }}
|
||
disabled={loading}
|
||
>
|
||
{limitOptions.map((option) => (
|
||
<MenuItem key={option} value={option}>
|
||
{option}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</Box>
|
||
<Typography variant="body2" color="textSecondary">
|
||
Всего: {totalCount}
|
||
</Typography>
|
||
</Box>
|
||
|
||
<Pagination
|
||
count={Math.ceil(totalCount / limit)}
|
||
page={page}
|
||
onChange={onPageChange}
|
||
color="primary"
|
||
size="large"
|
||
showFirstButton
|
||
showLastButton
|
||
disabled={loading}
|
||
renderItem={(item) => (
|
||
<PaginationItem
|
||
{...item}
|
||
sx={{
|
||
'&.Mui-selected': {
|
||
backgroundColor: 'primary.main',
|
||
color: 'white',
|
||
'&:hover': {
|
||
backgroundColor: 'primary.dark',
|
||
},
|
||
},
|
||
}}
|
||
/>
|
||
)}
|
||
/>
|
||
|
||
<Box sx={{ width: 120 }} />
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
);
|
||
} |