arm-core/web/src/layouts/ConsolidatedOsvPage.tsx

196 lines
7.3 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, Tooltip, message } from 'antd';
import { useCallback, 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';
import { useCurrentUser } from '../auth/MeContext';
import ChangeStatusModal from '../widgets/StatusChangeModal/ChangeStatusModal';
function OsvContent({ versionId }: { versionId: string }) {
const [card, setCard] = useState<VersionCard | null>(null);
const [statusModalOpen, setModalOpen] = useState(false);
const [exporting, setExporting] = useState(false);
// строка, для которой открыт выбор организации («Указать» или ИНН сопоставленного ЮЛ)
const [mapping, setMapping] = useState<ConsolidatedRowOut | null>(null);
const osv = useConsolidatedOsv(versionId);
const currentUser = useCurrentUser();
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} style={{ display: 'flex', gap: 4 }}>
<span style={{ color: '#667085', fontSize: 12 }}>Статус отчёта</span>
<TableTag
style={{
padding: '3px 10px',
width: 158,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 11,
}}
text={russianStatus}
{...preset}
/>
{card.lock === null || (card.lock && card.lock.locked_by !== currentUser.id) ? (
<Tooltip title='Сначала возьмите версию в работу'>
<Button size='middle' style={{ fontSize: 12 }} type='default' disabled>
Изменить статус
</Button>
</Tooltip>
) : (
<Button
size='middle'
onClick={() => {
setModalOpen(true);
}}
style={{ fontSize: 12 }}
type='default'>
Изменить статус
</Button>
)}
</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}
/>
)}
<ChangeStatusModal
open={statusModalOpen}
versionId={versionId}
onSuccess={setCard}
setClose={() => {
setModalOpen(false);
}}
/>
</div>
);
}
export function ConsolidatedOsvPage() {
const { version_id = '' } = useParams();
// key: при смене версии всё состояние (фильтры, страница, данные) создаётся заново
return <OsvContent key={version_id} versionId={version_id} />;
}