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;
+};