2026-07-02 15:35:44 +03:00

257 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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>
);
}