[1916] - отображение отчета после пробного прогона

This commit is contained in:
Veronika Tsygankova 2026-11-03 17:08:49 +03:00
parent 8ebc227979
commit 0a05716b09
6 changed files with 147 additions and 9 deletions

View File

@ -0,0 +1,16 @@
import { Typography } from 'antd';
type Props = {
label: string;
value: number;
};
/** Строка карточки: подпись серым, число справа — чёрным, если не 0. */
export function Counter({ label, value }: Props) {
return (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
<Typography.Text type='secondary'>{label}</Typography.Text>
<Typography.Text type={value === 0 ? 'secondary' : undefined}>{value}</Typography.Text>
</div>
);
}

View File

@ -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 (
<Card size='small' title={title} style={{ height: '100%' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{items.map((item) => (
<Counter key={item.label} label={item.label} value={item.value} />
))}
</div>
</Card>
);
}

View File

@ -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<Props> = ({ open, onCancel, onImported }) => {
const [form] = Form.useForm<FormValues>();
@ -35,6 +36,7 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [loading, setLoading] = useState(false);
const [dryRunReport, setDryRunReport] = useState<ImportReportOut | null>(null);
const handleOk = async () => {
const file = fileList[0]?.originFileObj;
@ -44,9 +46,9 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ 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<Props> = ({ open, onCancel, onIm
return (
<AppModal
title='Импорт групп компаний из файла'
title={dryRunReport ? 'Сводка импорта' : 'Импорт групп компаний из файла'}
open={open}
onCancel={handleCancel}
onOk={handleOk}
@ -75,11 +77,28 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
okButtonProps={{ disabled: fileList.length === 0 }}
cancelText='Отмена'
cancelButtonProps={{ disabled: loading }}
footer={
dryRunReport ? (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button icon={<ArrowLeftOutlined />} onClick={() => setDryRunReport(null)}>
К загрузке файла
</Button>
<Button type='primary' onClick={handleCancel}>
Закрыть
</Button>
</div>
) : undefined
}
confirmLoading={loading}
closable={!loading}
destroyOnHidden
width={720}>
<Form form={form} layout='vertical' disabled={loading} initialValues={{ on_existing: 'skip', dry_run: true }}>
width={dryRunReport ? 880 : 720}>
<Form
form={form}
layout='vertical'
disabled={loading}
initialValues={{ on_existing: 'skip', dry_run: true }}
style={{ display: dryRunReport ? 'none' : undefined }}>
<Form.Item name='on_existing' label='При дублировании' style={{ marginBottom: 12 }}>
<Select options={ON_EXISTING_OPTIONS} />
</Form.Item>
@ -109,6 +128,7 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
</Upload.Dragger>
</Form.Item>
</Form>
{dryRunReport && <ImportSummary report={dryRunReport} fileName={fileList[0]?.name ?? ''} />}
</AppModal>
);
};

View File

@ -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 (
<>
<Typography.Text type='secondary' style={{ display: 'block', marginBottom: 12 }}>
{fileName} · Предварительная проверка. Показано, какие изменения будут внесены.
</Typography.Text>
<Row gutter={12}>
<Col span={8}>
<CounterCard
title='Системные показатели'
items={[
{ label: 'Строк в файле', value: report.rows },
{ label: 'Пропущено строк с ошибками', value: report.errors },
{ label: 'РФ создано', value: report.offices_created },
]}
/>
</Col>
<Col span={8}>
<CounterCard
title='ГК'
items={[
{ label: 'ГК создано', value: report.groups_created },
{ label: 'ГК обновлено', value: report.groups_updated },
{ label: 'ГК пропущено', value: report.groups_skipped },
]}
/>
</Col>
<Col span={8}>
<CounterCard
title='Организации'
items={[
{ label: 'ЮЛ создано', value: report.entities_created },
{ label: 'ЮЛ обновлено', value: report.entities_updated },
{ label: 'ЮЛ пропущено', value: report.entities_skipped },
{ label: 'Включений в ГСК/ГСЗ', value: report.memberships_created },
]}
/>
</Col>
</Row>
<Typography.Title level={5} style={{ marginTop: 16 }}>
Строки с ошибками
</Typography.Title>
<Table
size='small'
// ключ — позиция: одна строка файла может дать несколько записей отчёта
dataSource={errorLines(report.lines).map((line, key) => ({ ...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' },
]}
/>
</>
);
}

View File

@ -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',
]);

View File

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