From 9fc054acd4f2fd2ccc7dec35501a3281c4da5372 Mon Sep 17 00:00:00 2001 From: Veronika Tsygankova Date: Thu, 4 Jun 2026 17:16:05 +0300 Subject: [PATCH] =?UTF-8?q?[AURORA-1057]=20-=20=D0=9E=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=B0?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/tasks.js | 8 +- web/src/components/common/ExportFormModal.jsx | 150 ++++++++++++++++++ .../components/common/TaskCardComponent.jsx | 11 +- web/src/pages/TaskPage/TaskPage.jsx | 20 ++- web/src/pages/TasksPage.jsx | 68 +++++--- web/src/utils/getApiErrorMessage.js | 26 +++ 6 files changed, 242 insertions(+), 41 deletions(-) create mode 100644 web/src/components/common/ExportFormModal.jsx create mode 100644 web/src/utils/getApiErrorMessage.js diff --git a/web/src/api/tasks.js b/web/src/api/tasks.js index f35713d..af20cd9 100644 --- a/web/src/api/tasks.js +++ b/web/src/api/tasks.js @@ -33,13 +33,15 @@ export const TasksApi = { createProjectTable(taskId, payload) { return api.post(`/tasks/${taskId}/projects`, payload).then((r) => r.data); }, - exportTask(taskId) { - return api.get(`export/task/${taskId}`, { + exportTask(taskId, params = {}) { + return api.get(`export/form/${taskId}`, { + params, responseType: "blob", }); }, - exportTasks(payload) { + exportTasks(payload, params = {}) { return api.post(`export/bulk`, payload, { + params, responseType: "blob", }); }, diff --git a/web/src/components/common/ExportFormModal.jsx b/web/src/components/common/ExportFormModal.jsx new file mode 100644 index 0000000..ab5ea79 --- /dev/null +++ b/web/src/components/common/ExportFormModal.jsx @@ -0,0 +1,150 @@ +import { useEffect, useState } from 'react'; +import { + Box, + Button, + FormControl, + FormLabel, + MenuItem, + Select, +} from '@mui/material'; +import Modal from './Modal/Modal'; +import { TasksApi } from '../../api/tasks'; +import { downloadFile } from '../../utils/downloadFile'; +import { getApiErrorMessage } from '../../utils/getApiErrorMessage'; +import { toast } from 'react-toastify'; + +const DIRECTION_OPTIONS = [ + { value: 'Development', label: 'Development' }, + { value: 'Support', label: 'Support' }, +]; + +export const ExportFormModal = ({ + open, + onClose, + onSuccess, + taskId, + taskTitle = 'form', + formIds, + fileName = 'Задачи', +}) => { + const isBulk = Boolean(formIds?.length); + const [direction, setDirection] = useState(''); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (open) { + setDirection(''); + setLoading(false); + } + }, [open]); + + const handleClose = () => { + if (!loading) { + onClose(); + } + }; + + const handleExport = async () => { + setLoading(true); + try { + const params = direction ? { direction } : {}; + + if (isBulk) { + const response = await TasksApi.exportTasks( + { form_ids: formIds }, + params, + ); + await downloadFile(response, fileName, 'xlsx'); + } else if (taskId) { + const response = await TasksApi.exportTask(taskId, params); + await downloadFile(response, taskTitle, 'xlsx'); + } + + onSuccess?.(); + onClose(); + } catch (error) { + const message = await getApiErrorMessage( + error, + 'Ошибка при скачивании файла', + ); + toast.error(message); + } finally { + setLoading(false); + } + }; + + return ( + + + + + } + > + + + direction + + + + + + + ); +}; diff --git a/web/src/components/common/TaskCardComponent.jsx b/web/src/components/common/TaskCardComponent.jsx index 4d83467..1dbc160 100644 --- a/web/src/components/common/TaskCardComponent.jsx +++ b/web/src/components/common/TaskCardComponent.jsx @@ -26,7 +26,6 @@ import { formatDate } from '../../utils/formatDate'; import { IconWithContent } from './IconWithContent'; import { Chip } from '../styles/StyledChip'; import { ExportButton } from '../common/Buttons/ButtonsActions'; -import { downloadFile } from '../../utils/downloadFile'; const TaskCard = styled.div` width: 26.8rem; @@ -93,6 +92,7 @@ export const TaskCardComponent = ({ exportMode = false, isLoadingFile = false, onAddForExport, + onExportClick, }) => { const navigate = useNavigate(); const { id, title, name, start_date, end_date, tables_count, org_unit, form_type_code } = taskForm; @@ -132,14 +132,9 @@ export const TaskCardComponent = ({ } }; - const handleExportTask = async (event) => { + const handleExportTask = (event) => { event.stopPropagation(); - try { - const response = await TasksApi.exportTask(id); - await downloadFile(response, title, 'xlsx'); - } catch (error) { - toast.error('Ошибка при скачивании файла'); - } + onExportClick?.(id, title); }; const updateTask = async (nextTitle) => { diff --git a/web/src/pages/TaskPage/TaskPage.jsx b/web/src/pages/TaskPage/TaskPage.jsx index 5b08735..7e740bc 100644 --- a/web/src/pages/TaskPage/TaskPage.jsx +++ b/web/src/pages/TaskPage/TaskPage.jsx @@ -19,8 +19,8 @@ import { SettingModal as SettingStageModal } from '../../components/Stages/Setti import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import { FormsApi } from '../../api/forms'; import { ModalCreateProject } from './ModalCreateProject'; -import { downloadFile } from '../../utils/downloadFile'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; +import { ExportFormModal } from '../../components/common/ExportFormModal'; import TablesList from '../../components/common/TableList/TableList'; import { SHEET_NAME } from '../../constants'; @@ -55,6 +55,7 @@ export default function TaskPage() { const [query, setQuery] = useState(''); const [modalStageOpen, setModalStageOpen] = useState(false); const [modalProjectOpen, setModalProjectOpen] = useState(false); + const [exportModalOpen, setExportModalOpen] = useState(false); const [tempProjects, setTempProjects] = useState([]); const [projects, setProjects] = useState([]); @@ -120,15 +121,6 @@ export default function TaskPage() { ); } - const handleExport = async () => { - try { - const response = await TasksApi.exportTask(taskId); - await downloadFile(response, task.title, 'xlsx'); - } catch (error) { - toast.error('Ошибка при скачивании файла'); - } - }; - return ( <> - + setExportModalOpen(true)} /> setModalStageOpen(true)} @@ -181,6 +173,12 @@ export default function TaskPage() { /> + setExportModalOpen(false)} + taskId={taskId} + taskTitle={task?.title ?? 'form'} + /> ); } \ No newline at end of file diff --git a/web/src/pages/TasksPage.jsx b/web/src/pages/TasksPage.jsx index 62453a5..8f5a579 100644 --- a/web/src/pages/TasksPage.jsx +++ b/web/src/pages/TasksPage.jsx @@ -27,11 +27,11 @@ import { ROLES_NAME_ID } from '../constants'; import { useAuth } from '../app/context/AuthProvider'; import { SspApi } from '../api/ssp'; import { PrimaryButton } from '../components/common/Buttons/Buttons'; -import { downloadFile } from '../utils/downloadFile'; import { ExportExitButton, ExportWithTextButton, } from '../components/common/Buttons/ButtonsActions'; +import { ExportFormModal } from '../components/common/ExportFormModal'; import { WhitePlus } from '../components/common/icons/icons'; export default function TasksPage() { @@ -60,7 +60,12 @@ export default function TasksPage() { const [exportMode, setExportMode] = useState(false); const [exportList, setExportList] = useState([]); - const [isLoadingFile, setIsLoadingFile] = useState(false); + const [exportModal, setExportModal] = useState({ + open: false, + taskId: null, + taskTitle: null, + formIds: null, + }); const openModal = () => { setModalOpen(true); @@ -185,16 +190,31 @@ export default function TasksPage() { } }; - const handleExport = async () => { - setIsLoadingFile(true); - try { - const response = await TasksApi.exportTasks({ task_ids: exportList }); - await downloadFile(response, 'Задачи', 'xlsx'); - setExportMode(false); - } catch (error) { - toast.error('Ошибка при скачивании файла'); - } - setIsLoadingFile(false); + const openBulkExportModal = () => { + setExportModal({ + open: true, + taskId: null, + taskTitle: null, + formIds: exportList, + }); + }; + + const openSingleExportModal = (taskId, taskTitle) => { + setExportModal({ + open: true, + taskId, + taskTitle, + formIds: null, + }); + }; + + const closeExportModal = () => { + setExportModal({ + open: false, + taskId: null, + taskTitle: null, + formIds: null, + }); }; // Calculate total pages @@ -236,13 +256,10 @@ export default function TasksPage() { <> - setExportMode(false)} - disabled={isLoadingFile} + onClick={openBulkExportModal} + disabled={exportList.length === 0} /> + setExportMode(false)} /> )} @@ -258,12 +275,12 @@ export default function TasksPage() { {items.map((t) => ( { handleChangeToExportList(t.id, checked); }} + onExportClick={openSingleExportModal} /> ))} @@ -444,6 +461,19 @@ export default function TasksPage() { + { + if (exportModal.formIds?.length) { + setExportMode(false); + } + closeExportModal(); + }} + taskId={exportModal.taskId} + taskTitle={exportModal.taskTitle ?? 'form'} + formIds={exportModal.formIds} + /> ); } diff --git a/web/src/utils/getApiErrorMessage.js b/web/src/utils/getApiErrorMessage.js new file mode 100644 index 0000000..fd932fe --- /dev/null +++ b/web/src/utils/getApiErrorMessage.js @@ -0,0 +1,26 @@ +export const getApiErrorMessage = async ( + error, + fallback = 'Произошла ошибка', +) => { + const data = error?.response?.data; + + if (!data) { + return error?.message || fallback; + } + + if (data instanceof Blob) { + try { + const text = await data.text(); + const json = JSON.parse(text); + return json.message || json.detail || text || fallback; + } catch { + return fallback; + } + } + + if (typeof data === 'string') { + return data || fallback; + } + + return data.message || data.detail || fallback; +};