Решения владельца 08.10.2026 (arm-core #74, отмена П-1): - ИНН из файла — просто текстом, без «Указать», даже если ЮЛ не найдено; - строка сопоставлена без ИНН в файле — «таблетка» с `counterparty_entity_inn`, клик открывает выбор организации, у текущего ЮЛ — неактивное «Связано» (закрывает D-1 из #16); - нет ни ЮЛ, ни ИНН — «Указать» на подсвеченной ячейке. Колонка ИНН — моноширинный шрифт по центру, воронка фильтра `unmapped`; баннер и кнопка «Показать ЮЛ без ИНН» — цвета `osv-mapping-alert` макета. Спека consolidated-osv-ui и README (1381 → 1260) обновлены. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qoydUVv8g1qHxc123u2RR
156 lines
6.1 KiB
TypeScript
156 lines
6.1 KiB
TypeScript
import { DownloadOutlined, FilterOutlined } from '@ant-design/icons';
|
||
import { Alert, Button, Space, message } from 'antd';
|
||
import { useEffect, useState } from 'react';
|
||
import { useParams } from 'react-router-dom';
|
||
import { ApiError } from '../api/client';
|
||
import PageTitle from '../components/PageTitle';
|
||
import { TableTag } from '../components/TableTag/TableTag';
|
||
import { TAG_PRESETS } from '../components/TableTag/presets';
|
||
import { getVersionCard } from '../services/api/consolidations';
|
||
import { exportOsv } from '../services/api/osv';
|
||
import type { ConsolidatedRowOut } from '../types/api';
|
||
import type { ConsRussianStatus } from '../types/consolidations/common_types';
|
||
import type { VersionCard } from '../types/consolidations/version_members';
|
||
import { exportFilename, saveBlob } from '../utils/download';
|
||
import { quarterShortLabel } from '../utils/labels';
|
||
import { statusColorMapping, statusMapping } from '../widgets/ConsoleTable/consts';
|
||
import { CounterpartyMapModal } from '../widgets/ConsolidatedOsv/CounterpartyMapModal';
|
||
import { OsvGaps } from '../widgets/ConsolidatedOsv/OsvGaps';
|
||
import { OsvTable } from '../widgets/ConsolidatedOsv/OsvTable';
|
||
import { OsvTabs } from '../widgets/ConsolidatedOsv/OsvTabs';
|
||
import { mapEntryId } from '../widgets/ConsolidatedOsv/counterpartyMapping';
|
||
import { OSV_PAGE_SIZE, useConsolidatedOsv } from '../widgets/ConsolidatedOsv/useConsolidatedOsv';
|
||
|
||
function OsvContent({ versionId }: { versionId: string }) {
|
||
const [card, setCard] = useState<VersionCard | null>(null);
|
||
const [exporting, setExporting] = useState(false);
|
||
// строка, для которой открыт выбор организации («Указать» или ИНН сопоставленного ЮЛ)
|
||
const [mapping, setMapping] = useState<ConsolidatedRowOut | null>(null);
|
||
const osv = useConsolidatedOsv(versionId);
|
||
|
||
useEffect(() => {
|
||
let actual = true;
|
||
getVersionCard(versionId)
|
||
.then((c) => {
|
||
if (actual) setCard(c);
|
||
})
|
||
.catch((e: unknown) => {
|
||
if (actual) message.error(e instanceof ApiError ? e.message : 'Не удалось получить версию');
|
||
});
|
||
return () => {
|
||
actual = false;
|
||
};
|
||
}, [versionId]);
|
||
|
||
const download = async () => {
|
||
setExporting(true);
|
||
try {
|
||
const { blob, filename } = await exportOsv(versionId, osv.filters);
|
||
saveBlob(blob, filename ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? ''));
|
||
} catch (e: unknown) {
|
||
message.error(e instanceof ApiError ? e.message : 'Не удалось выгрузить свод');
|
||
} finally {
|
||
setExporting(false);
|
||
}
|
||
};
|
||
|
||
const s = osv.summary;
|
||
const unmapped = s?.unmapped_total ?? 0;
|
||
const onlyUnmapped = osv.filters.unmapped === true;
|
||
const russianStatus = card ? (statusMapping[card.status] ?? card.status) : '';
|
||
const preset = statusColorMapping[russianStatus as ConsRussianStatus] ?? TAG_PRESETS.default;
|
||
const groupName = card?.company_group.name ?? '';
|
||
const period = card ? `${card.year} · ${quarterShortLabel(card.quarter)}` : '';
|
||
return (
|
||
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', height: '100vh', gap: 12 }}>
|
||
<PageTitle
|
||
backable
|
||
location={['Отчётность', groupName, period]}
|
||
header={card ? `${groupName} — ${card.year} ${quarterShortLabel(card.quarter)}` : 'Сводная ОСВ'}
|
||
description={russianStatus}
|
||
extra={
|
||
card && (
|
||
<Space size={8}>
|
||
<span style={{ color: '#667085', fontSize: 12 }}>Статус отчёта</span>
|
||
<TableTag text={russianStatus} {...preset} />
|
||
</Space>
|
||
)
|
||
}
|
||
/>
|
||
<OsvTabs />
|
||
<main
|
||
style={{
|
||
flex: '1 1 auto',
|
||
minHeight: 0,
|
||
background: 'white',
|
||
borderRadius: 12,
|
||
padding: 12,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 12,
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||
<Button icon={<DownloadOutlined />} loading={exporting} onClick={download}>
|
||
Экспорт
|
||
</Button>
|
||
</div>
|
||
{osv.error && <Alert type='error' showIcon title={osv.error} />}
|
||
{unmapped > 0 && (
|
||
// `osv-mapping-alert` макета [30] / [32-osv-no-inn]
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
flexWrap: 'wrap',
|
||
background: '#fffaeb',
|
||
border: '1px solid #fedf89',
|
||
borderRadius: 12,
|
||
padding: '10px 12px',
|
||
color: '#93370d',
|
||
fontSize: 13,
|
||
}}>
|
||
<strong style={{ color: '#7a2e0e' }}>Требуется указать соответствия</strong>
|
||
<span>Для перехода к следующему шагу сопоставьте все внешние организации без ИНН. Осталось: {unmapped}.</span>
|
||
{/* макет оставляет кнопку как есть, снимать фильтр — воронкой ИНН или «Сбросить фильтры» (панель — позже) */}
|
||
<Button
|
||
style={{ marginLeft: 'auto', minHeight: 30, borderColor: '#f4b740', background: '#fff', color: '#93370d' }}
|
||
icon={<FilterOutlined />}
|
||
onClick={() => osv.patchFilters({ unmapped: !onlyUnmapped })}>
|
||
{onlyUnmapped ? 'Показать все строки' : 'Показать ЮЛ без ИНН'}
|
||
</Button>
|
||
</div>
|
||
)}
|
||
<OsvGaps gaps={s?.gaps ?? []} />
|
||
<OsvTable
|
||
rows={osv.rows}
|
||
total={osv.total}
|
||
page={osv.page}
|
||
pageSize={OSV_PAGE_SIZE}
|
||
loading={osv.loading}
|
||
filters={osv.filters}
|
||
members={(card?.members ?? []).map((m) => ({ id: m.id, name: m.name }))}
|
||
onPage={osv.setPage}
|
||
onFilter={osv.patchFilters}
|
||
onMap={setMapping}
|
||
/>
|
||
</main>
|
||
{mapping && (
|
||
<CounterpartyMapModal
|
||
key={mapEntryId(mapping)}
|
||
versionId={versionId}
|
||
row={mapping}
|
||
onClose={() => setMapping(null)}
|
||
onMapped={osv.refresh}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ConsolidatedOsvPage() {
|
||
const { version_id = '' } = useParams();
|
||
// key: при смене версии всё состояние (фильтры, страница, данные) создаётся заново
|
||
return <OsvContent key={version_id} versionId={version_id} />;
|
||
}
|