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/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/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/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..3891cc5 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 { exportSingleForm } from '../../utils/exportForm'; import TablesList from '../../components/common/TableList/TableList'; import { SHEET_NAME } from '../../constants'; @@ -120,15 +120,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 ( <> - + exportSingleForm(taskId, task?.title ?? 'form')} + /> setModalStageOpen(true)} diff --git a/web/src/pages/TasksPage.jsx b/web/src/pages/TasksPage.jsx index 62453a5..f007b84 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 { exportBulkForms, exportSingleForm } from '../utils/exportForm'; import { WhitePlus } from '../components/common/icons/icons'; export default function TasksPage() { @@ -60,8 +60,6 @@ export default function TasksPage() { const [exportMode, setExportMode] = useState(false); const [exportList, setExportList] = useState([]); - const [isLoadingFile, setIsLoadingFile] = useState(false); - const openModal = () => { setModalOpen(true); }; @@ -185,16 +183,13 @@ 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 handleBulkExport = async () => { + await exportBulkForms(exportList); + setExportMode(false); + }; + + const handleSingleExport = (taskId, taskTitle) => { + exportSingleForm(taskId, taskTitle); }; // Calculate total pages @@ -236,13 +231,10 @@ export default function TasksPage() { <> - setExportMode(false)} - disabled={isLoadingFile} + onClick={handleBulkExport} + disabled={exportList.length === 0} /> + setExportMode(false)} /> )} @@ -258,12 +250,12 @@ export default function TasksPage() { {items.map((t) => ( { handleChangeToExportList(t.id, checked); }} + onExportClick={handleSingleExport} /> ))} 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; + } +}; 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; +};