список проектов
This commit is contained in:
parent
3c1722293c
commit
e28f6a3887
39
web/src/api/projects.js
Normal file
39
web/src/api/projects.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import api from "./client";
|
||||||
|
|
||||||
|
export const ProjectsApi = {
|
||||||
|
list: (params) => {
|
||||||
|
return api.get(`/projects`, { params }).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
get: (id) => {
|
||||||
|
return api.get(`/projects/${id}`).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
create: (data) => {
|
||||||
|
return api.post("/projects", data).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
update: (id, data) => {
|
||||||
|
return api.put(`/projects/${id}`, data).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: (id) => {
|
||||||
|
return api.delete(`/projects/${id}`).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
getTasks: (projectId) => {
|
||||||
|
return api.get(`/projects/${projectId}/tasks`).then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
addTask: (projectId, taskId) => {
|
||||||
|
return api
|
||||||
|
.post(`/projects/${projectId}/tasks/${taskId}`)
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTask: (projectId, taskId) => {
|
||||||
|
return api
|
||||||
|
.delete(`/projects/${projectId}/tasks/${taskId}`)
|
||||||
|
.then((r) => r.data);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -17,6 +17,7 @@ import { CircularProgress } from '@mui/material';
|
|||||||
import NewTablePage from '../pages/NewTablePage.jsx';
|
import NewTablePage from '../pages/NewTablePage.jsx';
|
||||||
import TablesTest from '../pages/TablesTest.jsx';
|
import TablesTest from '../pages/TablesTest.jsx';
|
||||||
import NewFormTablePage from '../pages/NewFormTablePage.jsx'
|
import NewFormTablePage from '../pages/NewFormTablePage.jsx'
|
||||||
|
import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx';
|
||||||
|
|
||||||
export const PrivateRoute = () => {
|
export const PrivateRoute = () => {
|
||||||
const { isAuthenticated, loading } = useAuth();
|
const { isAuthenticated, loading } = useAuth();
|
||||||
@ -37,6 +38,7 @@ export const AppRoutes = () => {
|
|||||||
<Route element={<PrivateRoute />}>
|
<Route element={<PrivateRoute />}>
|
||||||
<Route path="/" element={<TasksPage />} />
|
<Route path="/" element={<TasksPage />} />
|
||||||
<Route path="/tasks" element={<TasksPage />} />
|
<Route path="/tasks" element={<TasksPage />} />
|
||||||
|
<Route path="/projects" element={<ProjectsPage />}/>
|
||||||
<Route path="/forms" element={<FormsPage />} />
|
<Route path="/forms" element={<FormsPage />} />
|
||||||
<Route path="/task/:taskId" element={<TaskPage />} />
|
<Route path="/task/:taskId" element={<TaskPage />} />
|
||||||
<Route path="/table/:tableId" element={<TablePage />} />
|
<Route path="/table/:tableId" element={<TablePage />} />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { Box, Paper, Typography } from '@mui/material';
|
import { Box, Paper, Typography } from '@mui/material';
|
||||||
import { CustomCheckbox } from '../../components/common/StyledCheckbox';
|
import { CustomCheckbox } from '../StyledCheckbox';
|
||||||
|
|
||||||
export const OrgUnitsAutocompletePaper = forwardRef(
|
export const OrgUnitsAutocompletePaper = forwardRef(
|
||||||
function OrgUnitsAutocompletePaper(
|
function OrgUnitsAutocompletePaper(
|
||||||
247
web/src/components/common/PaginatedList.jsx
Normal file
247
web/src/components/common/PaginatedList.jsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
// 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/exportForm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Универсальный компонент для отображения списка с пагинацией и экспортом
|
||||||
|
*
|
||||||
|
* @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 - Включить скролл для списка
|
||||||
|
*/
|
||||||
|
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(110vh - 300px)',
|
||||||
|
enableScroll = true,
|
||||||
|
}) {
|
||||||
|
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%' }}>
|
||||||
|
{/* Верхняя панель с экспортом и дополнительными действиями */}
|
||||||
|
{enableExport && (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2, flexShrink: 0 }}>
|
||||||
|
{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)}
|
||||||
|
/>
|
||||||
|
{extraActions}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ExportWithTextButton
|
||||||
|
text="Экспортировать"
|
||||||
|
onClick={handleBulkExport}
|
||||||
|
disabled={exportList.length === 0}
|
||||||
|
/>
|
||||||
|
<ExportExitButton onClick={() => setExportMode(false)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
332
web/src/pages/ProjectsPage/ProjectCard.jsx
Normal file
332
web/src/pages/ProjectsPage/ProjectCard.jsx
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
// src/components/common/ProjectCardComponent/ProjectCardComponent.jsx
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Box,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ProjectsApi } from '../../api/projects';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import {
|
||||||
|
CheckBoxCheckSvg,
|
||||||
|
CheckBoxUncheckSvg,
|
||||||
|
DateIcon,
|
||||||
|
Edit,
|
||||||
|
ExportSvg,
|
||||||
|
UserIcon,
|
||||||
|
BuildingIcon,
|
||||||
|
LocationIcon,
|
||||||
|
} from '../../components/common/icons/icons';
|
||||||
|
import { IconWithContent } from '../../components/common/IconWithContent';
|
||||||
|
import { ExportButton } from '../../components/common/Buttons/ButtonsActions';
|
||||||
|
|
||||||
|
const ProjectCard = styled.div`
|
||||||
|
width: 26.8rem;
|
||||||
|
height: 15rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 1.25rem;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0.06rem solid var(--Stroke, rgba(0, 0, 0, 0.1));
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow:
|
||||||
|
0rem 2rem 4rem -2rem rgba(0, 0, 0, 0.02),
|
||||||
|
0rem 4rem 6rem -1rem rgba(0, 0, 0, 0.02);
|
||||||
|
background: var(--White, rgba(255, 255, 255, 1));
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardHeader = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectId = styled.div`
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1rem;
|
||||||
|
letter-spacing: 0rem;
|
||||||
|
text-align: left;
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardBody = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8125rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectTitle = styled.div`
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--Text-Primary, #333);
|
||||||
|
line-height: 1.63;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatusChip = styled(Chip)`
|
||||||
|
font-size: 0.7rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background-color: ${({ status }) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active': return '#4caf50';
|
||||||
|
case 'completed': return '#2196f3';
|
||||||
|
case 'paused': return '#ff9800';
|
||||||
|
case 'archived': return '#9e9e9e';
|
||||||
|
default: return '#9e9e9e';
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
color: white;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LevelChip = styled(Chip)`
|
||||||
|
font-size: 0.6rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ProjectInfoGrid = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const InfoChip = styled(Chip)`
|
||||||
|
font-size: 0.6rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
active: 'Активный',
|
||||||
|
completed: 'Завершен',
|
||||||
|
paused: 'Приостановлен',
|
||||||
|
archived: 'Архивирован',
|
||||||
|
};
|
||||||
|
return statusMap[status] || status || 'Не указан';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLevelLabel = (level) => {
|
||||||
|
const levelMap = {
|
||||||
|
project: 'Проект',
|
||||||
|
subproject: 'Подпроект',
|
||||||
|
task: 'Задача',
|
||||||
|
};
|
||||||
|
return levelMap[level] || level || 'Не указан';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectCardComponent = ({
|
||||||
|
project,
|
||||||
|
exportMode = false,
|
||||||
|
isLoadingFile = false,
|
||||||
|
onAddForExport,
|
||||||
|
onExportClick,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
org_unit_name,
|
||||||
|
year,
|
||||||
|
status,
|
||||||
|
level,
|
||||||
|
project_type,
|
||||||
|
vsp_format,
|
||||||
|
placement_type,
|
||||||
|
object_address,
|
||||||
|
staff_count,
|
||||||
|
total_area,
|
||||||
|
report_count,
|
||||||
|
parent_id,
|
||||||
|
} = project;
|
||||||
|
|
||||||
|
const [isEditModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [editedName, setEditedName] = useState('');
|
||||||
|
const [currentName, setCurrentName] = useState(name || '');
|
||||||
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsChecked(false);
|
||||||
|
}, [exportMode]);
|
||||||
|
|
||||||
|
const handleOpenEdit = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setEditedName(currentName);
|
||||||
|
setEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseEdit = () => {
|
||||||
|
setEditModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!isLoadingFile) {
|
||||||
|
setIsChecked(!isChecked);
|
||||||
|
onAddForExport(!isChecked);
|
||||||
|
} else {
|
||||||
|
toast.warning('Файл формируется!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExportProject = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onExportClick?.(id, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigate = () => {
|
||||||
|
if (!exportMode) {
|
||||||
|
navigate(`/project/${id}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProject = async (nextName) => {
|
||||||
|
try {
|
||||||
|
await ProjectsApi.update(id, { name: nextName });
|
||||||
|
setCurrentName(nextName);
|
||||||
|
setEditModalOpen(false);
|
||||||
|
toast.success('Название проекта обновлено');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error?.response?.data?.detail || 'Не удалось обновить название проекта',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveName = async () => {
|
||||||
|
const nextName = editedName.trim();
|
||||||
|
if (!nextName) {
|
||||||
|
toast.error('Название не может быть пустым');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nextName === currentName) {
|
||||||
|
setEditModalOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateProject(nextName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitials = (name) => {
|
||||||
|
if (!name) return '?';
|
||||||
|
const words = name.split(' ');
|
||||||
|
if (words.length >= 2) {
|
||||||
|
return `${words[0][0]}${words[1][0]}`.toUpperCase();
|
||||||
|
}
|
||||||
|
return name.substring(0, 2).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProjectCard onClick={handleNavigate} key={id}>
|
||||||
|
<Box sx={{ width: '100%' }}>
|
||||||
|
<CardHeader>
|
||||||
|
<ProjectId>ID: {id}</ProjectId>
|
||||||
|
<Stack direction={'row'} spacing={'.5rem'} alignItems="center">
|
||||||
|
{!exportMode && (
|
||||||
|
<>
|
||||||
|
{status && (
|
||||||
|
<StatusChip
|
||||||
|
status={status}
|
||||||
|
label={getStatusLabel(status)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ExportButton onClick={handleExportProject} />
|
||||||
|
<Edit onClick={handleOpenEdit} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<ProjectTitle>{currentName}</ProjectTitle>
|
||||||
|
|
||||||
|
<ProjectInfoGrid>
|
||||||
|
{level && (
|
||||||
|
<LevelChip label={getLevelLabel(level)} size="small" />
|
||||||
|
)}
|
||||||
|
{project_type && (
|
||||||
|
<InfoChip label={`Тип: ${project_type}`} size="small" />
|
||||||
|
)}
|
||||||
|
{report_count !== undefined && report_count !== null && (
|
||||||
|
<InfoChip label={`Отчетов: ${report_count}`} size="small" />
|
||||||
|
)}
|
||||||
|
{parent_id && (
|
||||||
|
<InfoChip label={`Родитель: ${parent_id}`} size="small" />
|
||||||
|
)}
|
||||||
|
</ProjectInfoGrid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
direction: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardBody>
|
||||||
|
<IconWithContent icon={<DateIcon />}>
|
||||||
|
{year || 'Год не указан'}
|
||||||
|
</IconWithContent>
|
||||||
|
<IconWithContent icon={<UserIcon />}>
|
||||||
|
{org_unit_name || 'Организация не указана'}
|
||||||
|
</IconWithContent>
|
||||||
|
{object_address && (
|
||||||
|
<IconWithContent >
|
||||||
|
{object_address}
|
||||||
|
</IconWithContent>
|
||||||
|
)}
|
||||||
|
{(staff_count || total_area) && (
|
||||||
|
<IconWithContent >
|
||||||
|
{staff_count && `👥 ${staff_count}`}
|
||||||
|
{staff_count && total_area && ' | '}
|
||||||
|
{total_area && `📐 ${total_area} м²`}
|
||||||
|
</IconWithContent>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
|
||||||
|
{exportMode && (
|
||||||
|
<div>
|
||||||
|
{isChecked ? (
|
||||||
|
<CheckBoxCheckSvg size="1.625rem" />
|
||||||
|
) : (
|
||||||
|
<CheckBoxUncheckSvg size="1.625rem" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ProjectCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
293
web/src/pages/ProjectsPage/ProjectsPage.jsx
Normal file
293
web/src/pages/ProjectsPage/ProjectsPage.jsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
// src/pages/ProjectsPage/ProjectsPage.jsx
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { ProjectsApi } from '../../api/projects';
|
||||||
|
import { CardsSkeleton } from '../../components/common/Skeleton';
|
||||||
|
import SearchComponent from '../../components/common/SearchComponent';
|
||||||
|
import { ProjectCardComponent } from './ProjectCard';
|
||||||
|
import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch';
|
||||||
|
import { PageContainer, TasksContainer } from '../StyledForPage';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
Button,
|
||||||
|
Stack,
|
||||||
|
Pagination,
|
||||||
|
PaginationItem,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Typography,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { ModalCreateProject } from './ModalCreateProject';
|
||||||
|
import {
|
||||||
|
ROLES_NAME_ID,
|
||||||
|
} from '../../constants';
|
||||||
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
|
import { PrimaryButton } from '../../components/common/Buttons/Buttons';
|
||||||
|
import {
|
||||||
|
ExportExitButton,
|
||||||
|
ExportWithTextButton,
|
||||||
|
} from '../../components/common/Buttons/ButtonsActions';
|
||||||
|
import { exportBulkForms, exportSingleForm } from '../../utils/exportForm';
|
||||||
|
import { WhitePlus } from '../../components/common/icons/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { PaginatedList } from '../../components/common/PaginatedList';
|
||||||
|
|
||||||
|
|
||||||
|
// Конфигурация для создания проекта
|
||||||
|
const defaultProjectConfig = {
|
||||||
|
'Тип проекта развития': {
|
||||||
|
type: 'multiselect',
|
||||||
|
options: [
|
||||||
|
'Открытие ВСП',
|
||||||
|
'Закрытие ВСП',
|
||||||
|
'Переезд ВСП',
|
||||||
|
'Реновация РФ',
|
||||||
|
'Открытие УРМ',
|
||||||
|
],
|
||||||
|
placeholder: 'Выберите тип проекта',
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
'Название проекта': {
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Введите название проекта',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
'Формат ВСП': {
|
||||||
|
type: 'multiselect',
|
||||||
|
options: [
|
||||||
|
'Фланговый',
|
||||||
|
'Типовой',
|
||||||
|
'Розничный',
|
||||||
|
'МСБ',
|
||||||
|
'Лёгкий',
|
||||||
|
'Мини',
|
||||||
|
'Розничный-киоск',
|
||||||
|
'МБО',
|
||||||
|
'Офис самообслуживания',
|
||||||
|
'Другое',
|
||||||
|
],
|
||||||
|
placeholder: 'Выберите формат ВСП',
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
'Адрес объекта': {
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Введите адрес объекта',
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
'Целевая численность, чел.': {
|
||||||
|
type: 'number',
|
||||||
|
placeholder: 'Введите число',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
'Тип размещения': {
|
||||||
|
type: 'multiselect',
|
||||||
|
options: ['Собственность', 'Аренда', 'Субаренда'],
|
||||||
|
placeholder: 'Выберите тип размещения',
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
'Площадь размещения, м2': {
|
||||||
|
type: 'number',
|
||||||
|
placeholder: 'Введите число',
|
||||||
|
defaultValue: 0,
|
||||||
|
min: 0,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProjectsPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
|
const [levelFilter, setLevelFilter] = useState('');
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [limit, setLimit] = useState(20);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const [exportMode, setExportMode] = useState(false);
|
||||||
|
const [exportList, setExportList] = useState([]);
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Маппинг полей из модалки в API
|
||||||
|
const mapFormDataToApi = (formData) => {
|
||||||
|
const apiData = {
|
||||||
|
name: formData['Название проекта'] || '',
|
||||||
|
project_type: formData['Тип проекта развития'] || null,
|
||||||
|
vsp_format: formData['Формат ВСП'] || null,
|
||||||
|
object_address: formData['Адрес объекта'] || '',
|
||||||
|
staff_count: formData['Целевая численность, чел.'] || 0,
|
||||||
|
placement_type: formData['Тип размещения'] || null,
|
||||||
|
total_area: formData['Площадь размещения, м2'] || 0,
|
||||||
|
// Добавляем значения по умолчанию для обязательных полей
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
org_unit_id: user?.org_unit_id || null,
|
||||||
|
form_type_code: 'project', // или другое значение по умолчанию
|
||||||
|
status: 'active',
|
||||||
|
level: 'project',
|
||||||
|
};
|
||||||
|
return apiData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateProject = async (formData) => {
|
||||||
|
try {
|
||||||
|
const apiData = mapFormDataToApi(formData);
|
||||||
|
|
||||||
|
await ProjectsApi.create(apiData);
|
||||||
|
|
||||||
|
setPage(1);
|
||||||
|
loadProjects(1, limit);
|
||||||
|
|
||||||
|
setModalOpen(false);
|
||||||
|
toast.success('Проект успешно создан');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
error.response?.data?.message ||
|
||||||
|
'Ошибка при создании проекта',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProjects = async (currentPage = page, currentLimit = limit) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const skip = (currentPage - 1) * currentLimit;
|
||||||
|
const params = {
|
||||||
|
offset: skip,
|
||||||
|
limit: currentLimit,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await ProjectsApi.list(params);
|
||||||
|
const data = res.result;
|
||||||
|
|
||||||
|
setItems(data);
|
||||||
|
setTotalCount(res.count);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Ошибка загрузки проектов');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
loadProjects(1, limit);
|
||||||
|
}, [query, statusFilter, levelFilter]);
|
||||||
|
|
||||||
|
const handlePageChange = (event, value) => {
|
||||||
|
setPage(value);
|
||||||
|
loadProjects(value, limit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLimitChange = (event) => {
|
||||||
|
const newLimit = event.target.value;
|
||||||
|
setLimit(newLimit);
|
||||||
|
setPage(1);
|
||||||
|
loadProjects(1, newLimit);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProjects(page, limit);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExportList([]);
|
||||||
|
}, [exportMode]);
|
||||||
|
|
||||||
|
const handleChangeToExportList = (projectId, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setExportList((prev) => [...prev, projectId]);
|
||||||
|
} else {
|
||||||
|
const newExportList = exportList.filter((t) => t != projectId);
|
||||||
|
setExportList(newExportList);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkExport = async () => {
|
||||||
|
await exportBulkForms(exportList);
|
||||||
|
setExportMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSingleExport = (projectId, projectTitle) => {
|
||||||
|
exportSingleForm(projectId, projectTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigateToTasks = () => {
|
||||||
|
navigate('/tasks');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
loadProjects(page, limit);
|
||||||
|
toast.info('Список проектов обновлен');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContainer>
|
||||||
|
<HeaderSwitch />
|
||||||
|
|
||||||
|
{/* Навигация */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||||
|
<Button variant="outlined" size="small" onClick={handleNavigateToTasks}>
|
||||||
|
← К задачам
|
||||||
|
</Button>
|
||||||
|
<Typography variant="h6" color="primary" sx={{ fontWeight: 500 }}>
|
||||||
|
Управление проектами
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<PaginatedList
|
||||||
|
items={items}
|
||||||
|
loading={loading}
|
||||||
|
totalCount={totalCount}
|
||||||
|
page={page}
|
||||||
|
limit={limit}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onLimitChange={handleLimitChange}
|
||||||
|
entityName="проектов"
|
||||||
|
emptyMessage="Проекты не найдены"
|
||||||
|
renderItem={(project, exportProps) => (
|
||||||
|
<ProjectCardComponent
|
||||||
|
key={project.id}
|
||||||
|
project={project}
|
||||||
|
{...exportProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
getItemId={(project) => project.id}
|
||||||
|
getItemTitle={(project) => project.name || 'Проект'}
|
||||||
|
extraActions={
|
||||||
|
user.role_id === ROLES_NAME_ID.admin && (
|
||||||
|
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />}>
|
||||||
|
Создать проект
|
||||||
|
</PrimaryButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageContainer>
|
||||||
|
|
||||||
|
<ModalCreateProject
|
||||||
|
open={modalOpen}
|
||||||
|
onClose={() => setModalOpen(false)}
|
||||||
|
onCreate={handleCreateProject}
|
||||||
|
config={defaultProjectConfig}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -18,7 +18,6 @@ import { toast } from 'react-toastify';
|
|||||||
import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal';
|
import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal';
|
||||||
import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
|
||||||
import { FormsApi } from '../../api/forms';
|
import { FormsApi } from '../../api/forms';
|
||||||
import { ModalCreateProject } from './ModalCreateProject';
|
|
||||||
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
|
||||||
import { exportSingleForm } from '../../utils/exportForm';
|
import { exportSingleForm } from '../../utils/exportForm';
|
||||||
import TablesList from '../../components/common/TableList/TableList';
|
import TablesList from '../../components/common/TableList/TableList';
|
||||||
|
|||||||
@ -37,8 +37,9 @@ import {
|
|||||||
} from '../../components/common/Buttons/ButtonsActions';
|
} from '../../components/common/Buttons/ButtonsActions';
|
||||||
import { exportBulkForms, exportSingleForm } from '../../utils/exportForm';
|
import { exportBulkForms, exportSingleForm } from '../../utils/exportForm';
|
||||||
import { WhitePlus } from '../../components/common/icons/icons';
|
import { WhitePlus } from '../../components/common/icons/icons';
|
||||||
import { OrgUnitsAutocompletePaper } from './OrgUnitsAutocompletePaper';
|
import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper';
|
||||||
import { renderOrgUnitValue } from './renderOrgUnitValue';
|
import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue';
|
||||||
|
import { PaginatedList } from '../../components/common/PaginatedList';
|
||||||
|
|
||||||
const modalSelectMenuProps = {
|
const modalSelectMenuProps = {
|
||||||
sx: { zIndex: 1301 },
|
sx: { zIndex: 1301 },
|
||||||
@ -255,128 +256,35 @@ export default function TasksPage() {
|
|||||||
<>
|
<>
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<HeaderSwitch />
|
<HeaderSwitch />
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
<HeaderSwitch />
|
||||||
{exportMode && <span>Выделено задач: {exportList.length}</span>}
|
|
||||||
</Box>
|
<PaginatedList
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
items={items}
|
||||||
<div style={{ width: '20rem' }}>
|
loading={loading}
|
||||||
{/* <SearchComponent
|
totalCount={totalCount}
|
||||||
placeholder="Поиск по названию задачи"
|
page={page}
|
||||||
value={query}
|
limit={limit}
|
||||||
onChange={setQuery}
|
onPageChange={handlePageChange}
|
||||||
/> */}
|
onLimitChange={handleLimitChange}
|
||||||
</div>
|
entityName="задач"
|
||||||
<Stack direction={'row'} spacing={'.5rem'}>
|
emptyMessage="Задачи не найдены"
|
||||||
{!exportMode ? (
|
renderItem={(task, exportProps) => (
|
||||||
<>
|
<TaskCardComponent
|
||||||
<ExportWithTextButton
|
key={task.id}
|
||||||
text={'Пакетный экспорт'}
|
taskForm={task}
|
||||||
onClick={() => {
|
{...exportProps}
|
||||||
setExportMode(true);
|
/>
|
||||||
}}
|
)}
|
||||||
></ExportWithTextButton>
|
getItemId={(task) => task.id}
|
||||||
{user.role_id == ROLES_NAME_ID.admin && (
|
getItemTitle={(task) => task.title || 'Задача'}
|
||||||
|
extraActions={
|
||||||
|
user.role_id === ROLES_NAME_ID.admin && (
|
||||||
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />}>
|
<PrimaryButton onClick={openModal} startIcon={<WhitePlus />}>
|
||||||
Создать задачу
|
Создать задачу
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
)}
|
)
|
||||||
</>
|
}
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExportWithTextButton
|
|
||||||
text={'Экспортировать'}
|
|
||||||
onClick={handleBulkExport}
|
|
||||||
disabled={exportList.length === 0}
|
|
||||||
/>
|
/>
|
||||||
<ExportExitButton onClick={() => setExportMode(false)} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* TODO: вернуть, когда будет реализовано на бэке */}
|
|
||||||
{/* <FilterComponent statusFilter={statusFilter} setStatusFilter={setStatusFilter} /> */}
|
|
||||||
<TasksContainer>
|
|
||||||
{loading ? (
|
|
||||||
<CardsSkeleton count={limit} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{items.map((t) => (
|
|
||||||
<TaskCardComponent
|
|
||||||
exportMode={exportMode}
|
|
||||||
taskForm={t}
|
|
||||||
key={t.id + '-task-id'}
|
|
||||||
onAddForExport={(checked) => {
|
|
||||||
handleChangeToExportList(t.id, checked);
|
|
||||||
}}
|
|
||||||
onExportClick={handleSingleExport}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TasksContainer>
|
|
||||||
|
|
||||||
{/* Pagination Footer */}
|
|
||||||
{!loading && items.length > 0 && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
pt: 2,
|
|
||||||
pb: 2,
|
|
||||||
borderTop: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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={handleLimitChange}
|
|
||||||
size="small"
|
|
||||||
sx={{ minWidth: 70 }}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<MenuItem value={10}>10</MenuItem>
|
|
||||||
<MenuItem value={20}>20</MenuItem>
|
|
||||||
<MenuItem value={50}>50</MenuItem>
|
|
||||||
<MenuItem value={100}>100</MenuItem>
|
|
||||||
<MenuItem value={200}>200</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Pagination
|
|
||||||
count={Math.ceil(totalCount / limit)}
|
|
||||||
page={page}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
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 }} /> {/* Spacer for symmetry */}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
<Modal
|
<Modal
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user