список проектов

This commit is contained in:
PotapovaA 2026-06-29 19:22:41 +03:00
parent 3c1722293c
commit e28f6a3887
10 changed files with 944 additions and 124 deletions

39
web/src/api/projects.js Normal file
View 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);
},
};

View File

@ -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 />} />

View File

@ -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(

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

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

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

View File

@ -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';

View File

@ -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}