arm-core/web/src/layouts/ConsolidatedOsvPage.tsx
2026-10-08 18:09:07 +03:00

154 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
flexWrap: 'wrap',
background: '#fffbe6',
border: '1px solid #ffe58f',
borderRadius: 8,
padding: '8px 12px',
color: '#ad6800',
}}>
<strong>Требуется указать соответствия</strong>
<span>Для перехода к следующему шагу сопоставьте все внешние организации без ИНН. Осталось: {unmapped}.</span>
<Button
style={{ marginLeft: 'auto' }}
icon={<FilterOutlined />}
type={onlyUnmapped ? 'primary' : 'default'}
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} />;
}