[1916] - отображение отчета после пробного прогона
This commit is contained in:
parent
8ebc227979
commit
0a05716b09
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 { Button, Checkbox, Form, Select, Upload } from 'antd';
|
||||||
import type { UploadFile } from 'antd/es/upload/interface';
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
@ -6,8 +6,9 @@ import { useState } from 'react';
|
|||||||
import { ApiError } from '../../../api/client';
|
import { ApiError } from '../../../api/client';
|
||||||
import { AppModal } from '../../../components/AppModal/AppModal';
|
import { AppModal } from '../../../components/AppModal/AppModal';
|
||||||
import { importDirectory } from '../../../services/api/directories/directory_import';
|
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 { useNotificationApi } from '../../../utils/useNotification';
|
||||||
|
import { ImportSummary } from './ImportSummary';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
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 }) => {
|
export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onImported }) => {
|
||||||
const [form] = Form.useForm<FormValues>();
|
const [form] = Form.useForm<FormValues>();
|
||||||
@ -35,6 +36,7 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
|
|||||||
|
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [dryRunReport, setDryRunReport] = useState<ImportReportOut | null>(null);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
const file = fileList[0]?.originFileObj;
|
const file = fileList[0]?.originFileObj;
|
||||||
@ -44,9 +46,9 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
|
|||||||
const values = form.getFieldsValue();
|
const values = form.getFieldsValue();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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) {
|
if (values.dry_run) {
|
||||||
notification.info({ description: 'Пробный прогон завершён: в справочник ничего не записано' });
|
setDryRunReport(report);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
notification.success({ description: 'Импорт завершён' });
|
notification.success({ description: 'Импорт завершён' });
|
||||||
@ -67,7 +69,7 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppModal
|
<AppModal
|
||||||
title='Импорт групп компаний из файла'
|
title={dryRunReport ? 'Сводка импорта' : 'Импорт групп компаний из файла'}
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
@ -75,11 +77,28 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
|
|||||||
okButtonProps={{ disabled: fileList.length === 0 }}
|
okButtonProps={{ disabled: fileList.length === 0 }}
|
||||||
cancelText='Отмена'
|
cancelText='Отмена'
|
||||||
cancelButtonProps={{ disabled: loading }}
|
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}
|
confirmLoading={loading}
|
||||||
closable={!loading}
|
closable={!loading}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
width={720}>
|
width={dryRunReport ? 880 : 720}>
|
||||||
<Form form={form} layout='vertical' disabled={loading} initialValues={{ on_existing: 'skip', dry_run: true }}>
|
<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 }}>
|
<Form.Item name='on_existing' label='При дублировании' style={{ marginBottom: 12 }}>
|
||||||
<Select options={ON_EXISTING_OPTIONS} />
|
<Select options={ON_EXISTING_OPTIONS} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -109,6 +128,7 @@ export const ImportCompanyGroupsModal: React.FC<Props> = ({ open, onCancel, onIm
|
|||||||
</Upload.Dragger>
|
</Upload.Dragger>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
{dryRunReport && <ImportSummary report={dryRunReport} fileName={fileList[0]?.name ?? ''} />}
|
||||||
</AppModal>
|
</AppModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
]);
|
||||||
@ -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));
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user