From e28f6a38871238fd4e8201753c9c6da15c8bbb54 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Mon, 29 Jun 2026 19:22:41 +0300 Subject: [PATCH] =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/projects.js | 39 ++ web/src/app/Routes.jsx | 2 + .../OrgUnitsAutocompletePaper.jsx | 2 +- .../renderOrgUnitValue.jsx | 0 web/src/components/common/PaginatedList.jsx | 247 +++++++++++++ .../ModalCreateProject.jsx | 0 web/src/pages/ProjectsPage/ProjectCard.jsx | 332 ++++++++++++++++++ web/src/pages/ProjectsPage/ProjectsPage.jsx | 293 ++++++++++++++++ web/src/pages/TaskPage/TaskPage.jsx | 1 - web/src/pages/TasksPage/TasksPage.jsx | 152 ++------ 10 files changed, 944 insertions(+), 124 deletions(-) create mode 100644 web/src/api/projects.js rename web/src/{pages/TasksPage => components/common/OrgUnitsAutocompletePaper}/OrgUnitsAutocompletePaper.jsx (94%) rename web/src/{pages/TasksPage => components/common/OrgUnitsAutocompletePaper}/renderOrgUnitValue.jsx (100%) create mode 100644 web/src/components/common/PaginatedList.jsx rename web/src/pages/{TaskPage => ProjectsPage}/ModalCreateProject.jsx (100%) create mode 100644 web/src/pages/ProjectsPage/ProjectCard.jsx create mode 100644 web/src/pages/ProjectsPage/ProjectsPage.jsx diff --git a/web/src/api/projects.js b/web/src/api/projects.js new file mode 100644 index 0000000..218ec55 --- /dev/null +++ b/web/src/api/projects.js @@ -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); + }, +}; diff --git a/web/src/app/Routes.jsx b/web/src/app/Routes.jsx index 2069477..7b62aa3 100644 --- a/web/src/app/Routes.jsx +++ b/web/src/app/Routes.jsx @@ -17,6 +17,7 @@ import { CircularProgress } from '@mui/material'; import NewTablePage from '../pages/NewTablePage.jsx'; import TablesTest from '../pages/TablesTest.jsx'; import NewFormTablePage from '../pages/NewFormTablePage.jsx' +import ProjectsPage from '../pages/ProjectsPage/ProjectsPage.jsx'; export const PrivateRoute = () => { const { isAuthenticated, loading } = useAuth(); @@ -37,6 +38,7 @@ export const AppRoutes = () => { }> } /> } /> + }/> } /> } /> } /> diff --git a/web/src/pages/TasksPage/OrgUnitsAutocompletePaper.jsx b/web/src/components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper.jsx similarity index 94% rename from web/src/pages/TasksPage/OrgUnitsAutocompletePaper.jsx rename to web/src/components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper.jsx index 07d45cd..3b039b1 100644 --- a/web/src/pages/TasksPage/OrgUnitsAutocompletePaper.jsx +++ b/web/src/components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper.jsx @@ -1,6 +1,6 @@ import { forwardRef } from 'react'; import { Box, Paper, Typography } from '@mui/material'; -import { CustomCheckbox } from '../../components/common/StyledCheckbox'; +import { CustomCheckbox } from '../StyledCheckbox'; export const OrgUnitsAutocompletePaper = forwardRef( function OrgUnitsAutocompletePaper( diff --git a/web/src/pages/TasksPage/renderOrgUnitValue.jsx b/web/src/components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue.jsx similarity index 100% rename from web/src/pages/TasksPage/renderOrgUnitValue.jsx rename to web/src/components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue.jsx diff --git a/web/src/components/common/PaginatedList.jsx b/web/src/components/common/PaginatedList.jsx new file mode 100644 index 0000000..ce4ee52 --- /dev/null +++ b/web/src/components/common/PaginatedList.jsx @@ -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 ( + + {/* Верхняя панель с экспортом и дополнительными действиями */} + {enableExport && ( + + {exportMode && ( + + Выделено {entityName}: {exportList.length} + + )} + + + {!exportMode ? ( + <> + setExportMode(true)} + /> + {extraActions} + + ) : ( + <> + + setExportMode(false)} /> + + )} + + + )} + + {/* Контейнер со скроллом для списка */} + + {/* Список элементов */} + + {loading ? ( + + ) : items.length === 0 ? ( + + + {emptyMessage} + + + ) : ( + items.map((item) => renderItemWithExport(item)) + )} + + + + {/* Пагинация */} + {!loading && items.length > 0 && ( + + + + + Показать на странице: + + + + + Всего: {totalCount} + + + + ( + + )} + /> + + + + )} + + ); +} \ No newline at end of file diff --git a/web/src/pages/TaskPage/ModalCreateProject.jsx b/web/src/pages/ProjectsPage/ModalCreateProject.jsx similarity index 100% rename from web/src/pages/TaskPage/ModalCreateProject.jsx rename to web/src/pages/ProjectsPage/ModalCreateProject.jsx diff --git a/web/src/pages/ProjectsPage/ProjectCard.jsx b/web/src/pages/ProjectsPage/ProjectCard.jsx new file mode 100644 index 0000000..29f281c --- /dev/null +++ b/web/src/pages/ProjectsPage/ProjectCard.jsx @@ -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 ( + <> + + + + ID: {id} + + {!exportMode && ( + <> + {status && ( + + )} + + + + )} + + + + {currentName} + + + {level && ( + + )} + {project_type && ( + + )} + {report_count !== undefined && report_count !== null && ( + + )} + {parent_id && ( + + )} + + + + + + }> + {year || 'Год не указан'} + + }> + {org_unit_name || 'Организация не указана'} + + {object_address && ( + + {object_address} + + )} + {(staff_count || total_area) && ( + + {staff_count && `👥 ${staff_count}`} + {staff_count && total_area && ' | '} + {total_area && `📐 ${total_area} м²`} + + )} + + + {exportMode && ( +
+ {isChecked ? ( + + ) : ( + + )} +
+ )} +
+
+ + ); +}; \ No newline at end of file diff --git a/web/src/pages/ProjectsPage/ProjectsPage.jsx b/web/src/pages/ProjectsPage/ProjectsPage.jsx new file mode 100644 index 0000000..d4f59a0 --- /dev/null +++ b/web/src/pages/ProjectsPage/ProjectsPage.jsx @@ -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 ( + <> + + + + {/* Навигация */} + + + + Управление проектами + + + + ( + + )} + getItemId={(project) => project.id} + getItemTitle={(project) => project.name || 'Проект'} + extraActions={ + user.role_id === ROLES_NAME_ID.admin && ( + }> + Создать проект + + ) + } + /> + + + setModalOpen(false)} + onCreate={handleCreateProject} + config={defaultProjectConfig} + /> + + ); +} \ No newline at end of file diff --git a/web/src/pages/TaskPage/TaskPage.jsx b/web/src/pages/TaskPage/TaskPage.jsx index ae3fcb7..62b22e0 100644 --- a/web/src/pages/TaskPage/TaskPage.jsx +++ b/web/src/pages/TaskPage/TaskPage.jsx @@ -18,7 +18,6 @@ import { toast } from 'react-toastify'; import { SettingModal as SettingStageModal } from '../../components/Stages/SettingModal'; import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import { FormsApi } from '../../api/forms'; -import { ModalCreateProject } from './ModalCreateProject'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; import { exportSingleForm } from '../../utils/exportForm'; import TablesList from '../../components/common/TableList/TableList'; diff --git a/web/src/pages/TasksPage/TasksPage.jsx b/web/src/pages/TasksPage/TasksPage.jsx index 00f8933..aee48c2 100644 --- a/web/src/pages/TasksPage/TasksPage.jsx +++ b/web/src/pages/TasksPage/TasksPage.jsx @@ -37,8 +37,9 @@ import { } from '../../components/common/Buttons/ButtonsActions'; import { exportBulkForms, exportSingleForm } from '../../utils/exportForm'; import { WhitePlus } from '../../components/common/icons/icons'; -import { OrgUnitsAutocompletePaper } from './OrgUnitsAutocompletePaper'; -import { renderOrgUnitValue } from './renderOrgUnitValue'; +import { OrgUnitsAutocompletePaper } from '../../components/common/OrgUnitsAutocompletePaper/OrgUnitsAutocompletePaper'; +import { renderOrgUnitValue } from '../../components/common/OrgUnitsAutocompletePaper/renderOrgUnitValue'; +import { PaginatedList } from '../../components/common/PaginatedList'; const modalSelectMenuProps = { sx: { zIndex: 1301 }, @@ -255,128 +256,35 @@ export default function TasksPage() { <> - - {exportMode && Выделено задач: {exportList.length}} - - -
- {/* */} -
- - {!exportMode ? ( - <> - { - setExportMode(true); - }} - > - {user.role_id == ROLES_NAME_ID.admin && ( - }> - Создать задачу - - )} - - ) : ( - <> - - setExportMode(false)} /> - - )} - -
+ - {/* TODO: вернуть, когда будет реализовано на бэке */} - {/* */} - - {loading ? ( - - ) : ( - <> - {items.map((t) => ( - { - handleChangeToExportList(t.id, checked); - }} - onExportClick={handleSingleExport} - /> - ))} - - )} - - - {/* Pagination Footer */} - {!loading && items.length > 0 && ( - - - - - Показать на странице: - - - - - ( - - )} + ( + - {/* Spacer for symmetry */} - - )} + )} + getItemId={(task) => task.id} + getItemTitle={(task) => task.title || 'Задача'} + extraActions={ + user.role_id === ROLES_NAME_ID.admin && ( + }> + Создать задачу + + ) + } + />