Merge pull request '[AURORA-1057] - экспорт форм' (#36) from AURORA-1057 into test

Reviewed-on: #36
This commit is contained in:
Veronika 2026-06-05 13:51:52 +03:00
commit ee6fafb84f
10 changed files with 132 additions and 51 deletions

View File

@ -6,4 +6,10 @@ export const FormsSheetApi = {
.get(`/form/${formId}/sheet/${sheetName}`, params) .get(`/form/${formId}/sheet/${sheetName}`, params)
.then((r) => r.data); .then((r) => r.data);
}, },
export(formId, sheetName, params = {}) {
return api.get(`export/form/${formId}/sheet/${sheetName}`, {
params,
responseType: 'blob',
});
},
}; };

View File

@ -33,13 +33,15 @@ export const TasksApi = {
createProjectTable(taskId, payload) { createProjectTable(taskId, payload) {
return api.post(`/tasks/${taskId}/projects`, payload).then((r) => r.data); return api.post(`/tasks/${taskId}/projects`, payload).then((r) => r.data);
}, },
exportTask(taskId) { exportTask(taskId, params = {}) {
return api.get(`export/task/${taskId}`, { return api.get(`export/form/${taskId}`, {
params,
responseType: "blob", responseType: "blob",
}); });
}, },
exportTasks(payload) { exportTasks(payload, params = {}) {
return api.post(`export/bulk`, payload, { return api.post(`export/bulk`, payload, {
params,
responseType: "blob", responseType: "blob",
}); });
}, },

View File

@ -251,6 +251,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
onAddRow={handleAddRow} onAddRow={handleAddRow}
onDeleteRow={handleDeleteRow} onDeleteRow={handleDeleteRow}
isLoadingData={isLoadingData} isLoadingData={isLoadingData}
formId={formId}
sheetName={sheetName}
direction={direction}
/> />
<div style={tableWrapperStyle}> <div style={tableWrapperStyle}>

View File

@ -17,6 +17,8 @@ import {
import CollapsibleTree from './CollapsibleTree'; import CollapsibleTree from './CollapsibleTree';
import ZoomSlider from './ZoomSlider'; import ZoomSlider from './ZoomSlider';
import SearchComponent from '../../common/SearchComponent'; import SearchComponent from '../../common/SearchComponent';
import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions';
import { exportSheet } from '../../../utils/exportForm';
const SettingPanel = ({ const SettingPanel = ({
selectedColumn, selectedColumn,
@ -32,8 +34,12 @@ const SettingPanel = ({
onAddRow, onAddRow,
onDeleteRow, onDeleteRow,
isLoadingData, isLoadingData,
formId,
sheetName,
direction,
}) => { }) => {
const [isPinned, setIsPinned] = useState(false); const [isPinned, setIsPinned] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [columnTree, setColumnTree] = useState(); const [columnTree, setColumnTree] = useState();
const [selectedColumnIds, setSelectedColumnIds] = useState(); const [selectedColumnIds, setSelectedColumnIds] = useState();
const [showColumnFilters, setShowColumnFilters] = useState(false); const [showColumnFilters, setShowColumnFilters] = useState(false);
@ -70,6 +76,13 @@ const SettingPanel = ({
const handleClickDeleteFormula = () => { }; const handleClickDeleteFormula = () => { };
const handleExport = async () => {
if (!formId || !sheetName || isExporting) return;
setIsExporting(true);
await exportSheet(formId, sheetName, direction);
setIsExporting(false);
};
return ( return (
<> <>
<Panel> <Panel>
@ -156,6 +169,14 @@ const SettingPanel = ({
</GroupByObject> </GroupByObject>
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
<GroupByObject title="Экспорт">
<ExportDefaultButton
onClick={handleExport}
disabled={!formId || !sheetName || isExporting}
text={isExporting ? 'Экспорт...' : 'Экспорт в Excel'}
/>
</GroupByObject>
</Stack> </Stack>
</Panel> </Panel>
</> </>

View File

@ -115,16 +115,11 @@ export const DefaultOutlinedButton = styled(Button)(({ width, height }) => ({
color: '#323031', color: '#323031',
'&:hover': { '&:hover': {
background: '#4a5565', background: '#f3f3f5',
color: 'white', borderColor: 'rgba(0, 0, 0, 0.2)',
'& svg path': {
color: 'white !important',
fill: 'white !important',
stroke: 'white !important',
},
}, },
'&:active': { '&:active': {
background: '#f3f3f5', background: '#e7e7ea',
}, },
'&.Mui-disabled': { '&.Mui-disabled': {

View File

@ -26,7 +26,6 @@ import { formatDate } from '../../utils/formatDate';
import { IconWithContent } from './IconWithContent'; import { IconWithContent } from './IconWithContent';
import { Chip } from '../styles/StyledChip'; import { Chip } from '../styles/StyledChip';
import { ExportButton } from '../common/Buttons/ButtonsActions'; import { ExportButton } from '../common/Buttons/ButtonsActions';
import { downloadFile } from '../../utils/downloadFile';
const TaskCard = styled.div` const TaskCard = styled.div`
width: 26.8rem; width: 26.8rem;
@ -93,6 +92,7 @@ export const TaskCardComponent = ({
exportMode = false, exportMode = false,
isLoadingFile = false, isLoadingFile = false,
onAddForExport, onAddForExport,
onExportClick,
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { id, title, name, start_date, end_date, tables_count, org_unit, form_type_code } = taskForm; 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(); event.stopPropagation();
try { onExportClick?.(id, title);
const response = await TasksApi.exportTask(id);
await downloadFile(response, title, 'xlsx');
} catch (error) {
toast.error('Ошибка при скачивании файла');
}
}; };
const updateTask = async (nextTitle) => { const updateTask = async (nextTitle) => {

View File

@ -19,8 +19,8 @@ import { SettingModal as SettingStageModal } from '../../components/Stages/Setti
import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons'; import { PrimaryOutlinedButton } from '../../components/common/Buttons/Buttons';
import { FormsApi } from '../../api/forms'; import { FormsApi } from '../../api/forms';
import { ModalCreateProject } from './ModalCreateProject'; import { ModalCreateProject } from './ModalCreateProject';
import { downloadFile } from '../../utils/downloadFile';
import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions';
import { exportSingleForm } from '../../utils/exportForm';
import TablesList from '../../components/common/TableList/TableList'; import TablesList from '../../components/common/TableList/TableList';
import { SHEET_NAME } from '../../constants'; 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 ( return (
<> <>
<Box <Box
@ -156,7 +147,9 @@ export default function TaskPage() {
onChange={setQuery} onChange={setQuery}
/> />
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end' }}> <Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end' }}>
<ExportWithTextButton onClick={handleExport} /> <ExportWithTextButton
onClick={() => exportSingleForm(taskId, task?.title ?? 'form')}
/>
<PrimaryOutlinedButton <PrimaryOutlinedButton
variant="outlined" variant="outlined"
onClick={() => setModalStageOpen(true)} onClick={() => setModalStageOpen(true)}

View File

@ -27,11 +27,11 @@ import { ROLES_NAME_ID } from '../constants';
import { useAuth } from '../app/context/AuthProvider'; import { useAuth } from '../app/context/AuthProvider';
import { SspApi } from '../api/ssp'; import { SspApi } from '../api/ssp';
import { PrimaryButton } from '../components/common/Buttons/Buttons'; import { PrimaryButton } from '../components/common/Buttons/Buttons';
import { downloadFile } from '../utils/downloadFile';
import { import {
ExportExitButton, ExportExitButton,
ExportWithTextButton, ExportWithTextButton,
} from '../components/common/Buttons/ButtonsActions'; } from '../components/common/Buttons/ButtonsActions';
import { exportBulkForms, exportSingleForm } from '../utils/exportForm';
import { WhitePlus } from '../components/common/icons/icons'; import { WhitePlus } from '../components/common/icons/icons';
export default function TasksPage() { export default function TasksPage() {
@ -60,8 +60,6 @@ export default function TasksPage() {
const [exportMode, setExportMode] = useState(false); const [exportMode, setExportMode] = useState(false);
const [exportList, setExportList] = useState([]); const [exportList, setExportList] = useState([]);
const [isLoadingFile, setIsLoadingFile] = useState(false);
const openModal = () => { const openModal = () => {
setModalOpen(true); setModalOpen(true);
}; };
@ -185,16 +183,13 @@ export default function TasksPage() {
} }
}; };
const handleExport = async () => { const handleBulkExport = async () => {
setIsLoadingFile(true); await exportBulkForms(exportList);
try {
const response = await TasksApi.exportTasks({ task_ids: exportList });
await downloadFile(response, 'Задачи', 'xlsx');
setExportMode(false); setExportMode(false);
} catch (error) { };
toast.error('Ошибка при скачивании файла');
} const handleSingleExport = (taskId, taskTitle) => {
setIsLoadingFile(false); exportSingleForm(taskId, taskTitle);
}; };
// Calculate total pages // Calculate total pages
@ -236,13 +231,10 @@ export default function TasksPage() {
<> <>
<ExportWithTextButton <ExportWithTextButton
text={'Экспортировать'} text={'Экспортировать'}
onClick={handleExport} onClick={handleBulkExport}
disabled={isLoadingFile} disabled={exportList.length === 0}
/>
<ExportExitButton
onClick={() => setExportMode(false)}
disabled={isLoadingFile}
/> />
<ExportExitButton onClick={() => setExportMode(false)} />
</> </>
)} )}
</Stack> </Stack>
@ -258,12 +250,12 @@ export default function TasksPage() {
{items.map((t) => ( {items.map((t) => (
<TaskCardComponent <TaskCardComponent
exportMode={exportMode} exportMode={exportMode}
isLoadingFile={exportMode ? isLoadingFile : false}
taskForm={t} taskForm={t}
key={t.id + '-task-id'} key={t.id + '-task-id'}
onAddForExport={(checked) => { onAddForExport={(checked) => {
handleChangeToExportList(t.id, checked); handleChangeToExportList(t.id, checked);
}} }}
onExportClick={handleSingleExport}
/> />
))} ))}
</> </>

View File

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

View File

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