From 9fc054acd4f2fd2ccc7dec35501a3281c4da5372 Mon Sep 17 00:00:00 2001 From: Veronika Tsygankova Date: Thu, 4 Jun 2026 17:16:05 +0300 Subject: [PATCH 1/2] =?UTF-8?q?[AURORA-1057]=20-=20=D0=9E=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BA=D0=B0=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82?= =?UTF-8?q?=D0=B0=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; +}; From e2bd55e5b184e21b77120764e10dd099f7d31adc Mon Sep 17 00:00:00 2001 From: Veronika Tsygankova Date: Fri, 5 Jun 2026 13:04:47 +0300 Subject: [PATCH 2/2] =?UTF-8?q?[AURORA-1057]=20-=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/form_sheet.js | 8 +- .../RealtimeTable/RealtimeTable.jsx | 3 + .../SettingPanel/SettingPanel.jsx | 21 +++ web/src/components/common/Buttons/Buttons.jsx | 11 +- web/src/components/common/ExportFormModal.jsx | 150 ------------------ web/src/pages/TaskPage/TaskPage.jsx | 13 +- web/src/pages/TasksPage.jsx | 54 +------ web/src/utils/exportForm.js | 48 ++++++ 8 files changed, 94 insertions(+), 214 deletions(-) delete mode 100644 web/src/components/common/ExportFormModal.jsx create mode 100644 web/src/utils/exportForm.js diff --git a/web/src/api/form_sheet.js b/web/src/api/form_sheet.js index 9213a35..4d6220e 100644 --- a/web/src/api/form_sheet.js +++ b/web/src/api/form_sheet.js @@ -3,7 +3,13 @@ import api from './client'; export const FormsSheetApi = { get(formId, sheetName, params) { return api - .get(`/form/${formId}/sheet/${sheetName}`,params) + .get(`/form/${formId}/sheet/${sheetName}`, params) .then((r) => r.data); }, + export(formId, sheetName, params = {}) { + return api.get(`export/form/${formId}/sheet/${sheetName}`, { + params, + responseType: 'blob', + }); + }, }; diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 654b88a..b860bca 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -251,6 +251,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => { onAddRow={handleAddRow} onDeleteRow={handleDeleteRow} isLoadingData={isLoadingData} + formId={formId} + sheetName={sheetName} + direction={direction} />
diff --git a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx index 6477c84..ecfa843 100644 --- a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx +++ b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx @@ -17,6 +17,8 @@ import { import CollapsibleTree from './CollapsibleTree'; import ZoomSlider from './ZoomSlider'; import SearchComponent from '../../common/SearchComponent'; +import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions'; +import { exportSheet } from '../../../utils/exportForm'; const SettingPanel = ({ selectedColumn, @@ -32,8 +34,12 @@ const SettingPanel = ({ onAddRow, onDeleteRow, isLoadingData, + formId, + sheetName, + direction, }) => { const [isPinned, setIsPinned] = useState(false); + const [isExporting, setIsExporting] = useState(false); const [columnTree, setColumnTree] = useState(); const [selectedColumnIds, setSelectedColumnIds] = useState(); const [showColumnFilters, setShowColumnFilters] = useState(false); @@ -70,6 +76,13 @@ const SettingPanel = ({ const handleClickDeleteFormula = () => { }; + const handleExport = async () => { + if (!formId || !sheetName || isExporting) return; + setIsExporting(true); + await exportSheet(formId, sheetName, direction); + setIsExporting(false); + }; + return ( <> @@ -156,6 +169,14 @@ const SettingPanel = ({ + + + + diff --git a/web/src/components/common/Buttons/Buttons.jsx b/web/src/components/common/Buttons/Buttons.jsx index 079463c..4242ccc 100644 --- a/web/src/components/common/Buttons/Buttons.jsx +++ b/web/src/components/common/Buttons/Buttons.jsx @@ -115,16 +115,11 @@ export const DefaultOutlinedButton = styled(Button)(({ width, height }) => ({ color: '#323031', '&:hover': { - background: '#4a5565', - color: 'white', - '& svg path': { - color: 'white !important', - fill: 'white !important', - stroke: 'white !important', - }, + background: '#f3f3f5', + borderColor: 'rgba(0, 0, 0, 0.2)', }, '&:active': { - background: '#f3f3f5', + background: '#e7e7ea', }, '&.Mui-disabled': { diff --git a/web/src/components/common/ExportFormModal.jsx b/web/src/components/common/ExportFormModal.jsx deleted file mode 100644 index ab5ea79..0000000 --- a/web/src/components/common/ExportFormModal.jsx +++ /dev/null @@ -1,150 +0,0 @@ -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/pages/TaskPage/TaskPage.jsx b/web/src/pages/TaskPage/TaskPage.jsx index 7e740bc..3891cc5 100644 --- a/web/src/pages/TaskPage/TaskPage.jsx +++ b/web/src/pages/TaskPage/TaskPage.jsx @@ -20,7 +20,7 @@ import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import { FormsApi } from '../../api/forms'; import { ModalCreateProject } from './ModalCreateProject'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; -import { ExportFormModal } from '../../components/common/ExportFormModal'; +import { exportSingleForm } from '../../utils/exportForm'; import TablesList from '../../components/common/TableList/TableList'; import { SHEET_NAME } from '../../constants'; @@ -55,7 +55,6 @@ 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([]); @@ -148,7 +147,9 @@ export default function TaskPage() { onChange={setQuery} /> - setExportModalOpen(true)} /> + exportSingleForm(taskId, task?.title ?? 'form')} + /> setModalStageOpen(true)} @@ -173,12 +174,6 @@ 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 8f5a579..f007b84 100644 --- a/web/src/pages/TasksPage.jsx +++ b/web/src/pages/TasksPage.jsx @@ -31,7 +31,7 @@ import { ExportExitButton, ExportWithTextButton, } from '../components/common/Buttons/ButtonsActions'; -import { ExportFormModal } from '../components/common/ExportFormModal'; +import { exportBulkForms, exportSingleForm } from '../utils/exportForm'; import { WhitePlus } from '../components/common/icons/icons'; export default function TasksPage() { @@ -60,13 +60,6 @@ export default function TasksPage() { const [exportMode, setExportMode] = useState(false); const [exportList, setExportList] = useState([]); - const [exportModal, setExportModal] = useState({ - open: false, - taskId: null, - taskTitle: null, - formIds: null, - }); - const openModal = () => { setModalOpen(true); }; @@ -190,31 +183,13 @@ export default function TasksPage() { } }; - const openBulkExportModal = () => { - setExportModal({ - open: true, - taskId: null, - taskTitle: null, - formIds: exportList, - }); + const handleBulkExport = async () => { + await exportBulkForms(exportList); + setExportMode(false); }; - const openSingleExportModal = (taskId, taskTitle) => { - setExportModal({ - open: true, - taskId, - taskTitle, - formIds: null, - }); - }; - - const closeExportModal = () => { - setExportModal({ - open: false, - taskId: null, - taskTitle: null, - formIds: null, - }); + const handleSingleExport = (taskId, taskTitle) => { + exportSingleForm(taskId, taskTitle); }; // Calculate total pages @@ -256,7 +231,7 @@ export default function TasksPage() { <> setExportMode(false)} /> @@ -280,7 +255,7 @@ export default function TasksPage() { onAddForExport={(checked) => { handleChangeToExportList(t.id, checked); }} - onExportClick={openSingleExportModal} + onExportClick={handleSingleExport} /> ))} @@ -461,19 +436,6 @@ 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/exportForm.js b/web/src/utils/exportForm.js new file mode 100644 index 0000000..56eba58 --- /dev/null +++ b/web/src/utils/exportForm.js @@ -0,0 +1,48 @@ +import { FormsSheetApi } from '../api/form_sheet'; +import { TasksApi } from '../api/tasks'; +import { downloadFile } from './downloadFile'; +import { getApiErrorMessage } from './getApiErrorMessage'; +import { toast } from 'react-toastify'; + +export const exportSingleForm = async (taskId, taskTitle) => { + try { + const response = await TasksApi.exportTask(taskId); + await downloadFile(response, taskTitle, 'xlsx'); + } catch (error) { + const message = await getApiErrorMessage( + error, + 'Ошибка при скачивании файла', + ); + toast.error(message); + } +}; + +export const exportSheet = async (formId, sheetName, direction) => { + try { + const normalizedDirection = + direction && direction !== 'null' ? direction : undefined; + const params = normalizedDirection ? { direction: normalizedDirection } : {}; + const response = await FormsSheetApi.export(formId, sheetName, params); + await downloadFile(response, sheetName, 'xlsx'); + } catch (error) { + const message = await getApiErrorMessage( + error, + 'Ошибка при скачивании файла', + ); + toast.error(message); + } +}; + +export const exportBulkForms = async (formIds, fileName = 'Задачи') => { + try { + const response = await TasksApi.exportTasks({ form_ids: formIds }); + await downloadFile(response, fileName, 'xlsx'); + } catch (error) { + const message = await getApiErrorMessage( + error, + 'Ошибка при скачивании файла', + ); + toast.error(message); + throw error; + } +};