diff --git a/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/Counter.tsx b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/Counter.tsx
new file mode 100644
index 0000000..5c8880d
--- /dev/null
+++ b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/Counter.tsx
@@ -0,0 +1,16 @@
+import { Typography } from 'antd';
+
+type Props = {
+ label: string;
+ value: number;
+};
+
+/** Строка карточки: подпись серым, число справа — чёрным, если не 0. */
+export function Counter({ label, value }: Props) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
diff --git a/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/CounterCard.tsx b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/CounterCard.tsx
new file mode 100644
index 0000000..577ba6c
--- /dev/null
+++ b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/CounterCard.tsx
@@ -0,0 +1,19 @@
+import { Card } from 'antd';
+import { Counter } from './Counter';
+
+type Props = {
+ title: string;
+ items: { label: string; value: number }[];
+};
+
+export function CounterCard({ title, items }: Props) {
+ return (
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+ );
+}
diff --git a/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/ImportCompanyGroupsModal.tsx b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/ImportCompanyGroupsModal.tsx
index 420bb53..0e3ff9e 100644
--- a/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/ImportCompanyGroupsModal.tsx
+++ b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/ImportCompanyGroupsModal.tsx
@@ -1,4 +1,4 @@
-import { FileAddOutlined } from '@ant-design/icons';
+import { ArrowLeftOutlined, FileAddOutlined } from '@ant-design/icons';
import { Button, Checkbox, Form, Select, Upload } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface';
import type React from 'react';
@@ -6,8 +6,9 @@ import { useState } from 'react';
import { ApiError } from '../../../api/client';
import { AppModal } from '../../../components/AppModal/AppModal';
import { importDirectory } from '../../../services/api/directories/directory_import';
-import type { OnExisting } from '../../../types/api';
+import type { ImportReportOut, OnExisting } from '../../../types/api';
import { useNotificationApi } from '../../../utils/useNotification';
+import { ImportSummary } from './ImportSummary';
type Props = {
open: boolean;
@@ -26,8 +27,8 @@ const ON_EXISTING_OPTIONS: { value: OnExisting; label: string }[] = [
];
/**
- * Импорт выгрузки ЕЦП CRM. Сценарий двухшаговый: после пробного прогона форма и файл остаются, чтобы снять галочку
- * и записать тем же файлом; после записи — уведомление и закрытие окна.
+ * Импорт выгрузки ЕЦП CRM. Пробный прогон заменяет форму сводкой: из неё — закрытие окна или возврат к форме
+ * («К загрузке файла», файл и значения формы сохраняются); после записи — уведомление и закрытие окна.
*/
export const ImportCompanyGroupsModal: React.FC = ({ open, onCancel, onImported }) => {
const [form] = Form.useForm();
@@ -35,6 +36,7 @@ export const ImportCompanyGroupsModal: React.FC = ({ open, onCancel, onIm
const [fileList, setFileList] = useState([]);
const [loading, setLoading] = useState(false);
+ const [dryRunReport, setDryRunReport] = useState(null);
const handleOk = async () => {
const file = fileList[0]?.originFileObj;
@@ -44,9 +46,9 @@ export const ImportCompanyGroupsModal: React.FC = ({ open, onCancel, onIm
const values = form.getFieldsValue();
setLoading(true);
try {
- await importDirectory(file, values.dry_run, values.on_existing);
+ const report = await importDirectory(file, values.dry_run, values.on_existing);
if (values.dry_run) {
- notification.info({ description: 'Пробный прогон завершён: в справочник ничего не записано' });
+ setDryRunReport(report);
return;
}
notification.success({ description: 'Импорт завершён' });
@@ -67,7 +69,7 @@ export const ImportCompanyGroupsModal: React.FC = ({ open, onCancel, onIm
return (
= ({ open, onCancel, onIm
okButtonProps={{ disabled: fileList.length === 0 }}
cancelText='Отмена'
cancelButtonProps={{ disabled: loading }}
+ footer={
+ dryRunReport ? (
+
+ } onClick={() => setDryRunReport(null)}>
+ К загрузке файла
+
+
+
+ ) : undefined
+ }
confirmLoading={loading}
closable={!loading}
destroyOnHidden
- width={720}>
-
@@ -109,6 +128,7 @@ export const ImportCompanyGroupsModal: React.FC = ({ open, onCancel, onIm
+ {dryRunReport && }
);
};
diff --git a/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/ImportSummary.tsx b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/ImportSummary.tsx
new file mode 100644
index 0000000..9c810fa
--- /dev/null
+++ b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/ImportSummary.tsx
@@ -0,0 +1,68 @@
+import { Col, Row, Table, Typography } from 'antd';
+import type { ImportReportOut } from '../../../types/api';
+import { CounterCard } from './CounterCard';
+import { errorLines } from './utils';
+
+type Props = {
+ report: ImportReportOut;
+ fileName: string;
+};
+
+/** Сводка пробного прогона: счётчики по трём карточкам и строки, пропущенные из-за ошибок. */
+export function ImportSummary({ report, fileName }: Props) {
+ return (
+ <>
+
+ {fileName} · Предварительная проверка. Показано, какие изменения будут внесены.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Строки с ошибками
+
+ ({ ...line, key }))}
+ pagination={{ pageSize: 5, hideOnSinglePage: true, showSizeChanger: false }}
+ locale={{ emptyText: 'Ошибок не найдено' }}
+ columns={[
+ { title: 'Строка', dataIndex: 'row', width: 80 },
+ { title: 'ИНН', dataIndex: 'inn', width: 140 },
+ { title: 'Результат', dataIndex: 'message' },
+ ]}
+ />
+ >
+ );
+}
diff --git a/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/consts.ts b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/consts.ts
new file mode 100644
index 0000000..6e3891b
--- /dev/null
+++ b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/consts.ts
@@ -0,0 +1,9 @@
+/** Коды строк, пропущенных импортом из-за ошибки (§13.2); остальные коды — успешные итоги и сопутствующие события. */
+export const ERROR_CODES = new Set([
+ 'UNSUPPORTED_TYPE',
+ 'INVALID_INN',
+ 'INVALID_KPP',
+ 'DUPLICATE_IN_FILE',
+ 'LE_IN_OTHER_GROUP',
+ 'GK_NAME_CONFLICT',
+]);
diff --git a/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/utils.ts b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/utils.ts
new file mode 100644
index 0000000..d263cb2
--- /dev/null
+++ b/web/src/widgets/CatalogueCompanyGroup/ImportCompanyGroupsModal/utils.ts
@@ -0,0 +1,6 @@
+import type { ImportReportLine } from '../../../types/api';
+import { ERROR_CODES } from './consts';
+
+export function errorLines(lines: ImportReportLine[]): ImportReportLine[] {
+ return lines.filter((line) => ERROR_CODES.has(line.code));
+}