arm-core/web/src/layouts/DocumentPage.tsx
Andrey Epifancev 175343b843 fix(web): вернуть синхронизацию фронта #139/#140, затёртую в ветке шахматки (ревью #158)
Коммит 7891430 откатил синхронизацию фронта (5bec434, #139/#140) и форматирование Prettier (#137).
Из origin/main восстановлены файлы, не относящиеся к шахматке:
- index.tsx (тема antd: Layout.fontSize, Form.labelFontSize/labelColor);
- layouts/DocumentPage.tsx (переключатель документа, комментарий к документу);
- services/api: consolidations.ts (getChecklist), documents.ts (postComment), files.ts (removeFile),
  directories/catalogue_jur.ts; types/consolidations/version_members.ts (ChecklistBackendShape);
- widgets: ConsoleLegalEntities (ChecklistModal, checklist_utils), FilesDrawer (DeleteModal, ReplaceDocModal,
  documentRows), DocumentReview (CommentModal, StatementReview, reviewCounters, reviewView),
  CatalogueJurForm, CatalogueJurTable, CompanyGroupsList;
- hooks/useVirtualList.tsx и components/TableTag/presets.tsx (форматирование Prettier).

В файлах шахматки оставлены её изменения: App.tsx, layouts/index.ts, api/client.ts (init в apiFetchBlob), OsvTabs.tsx,
ConsolidatedOsvContent.tsx. Из main возвращён key={version_id}: страница отчёта разделена на обёртку с key и
содержимое, чтобы при смене версии состояние (карточка, фильтры, страница) создавалось заново, как в ConsolidatedOsvPage.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 14:04:19 +04:00

140 lines
6.2 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 { CommentOutlined } from '@ant-design/icons';
import { Button, Empty, Form, Result, Select, Spin, Tooltip } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate, 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 { fetchLegalEntity } from '../services/api/directories/company_groups';
import { getDocument } from '../services/api/documents';
import { getFile, listLegalEntityFiles } from '../services/api/files';
import type { DocumentOut } from '../types/api';
import { documentTypeLabel, periodLabel } from '../utils/labels';
import CommentModal from '../widgets/DocumentReview/CommentModal';
import { OsvDocument } from '../widgets/DocumentReview/OsvDocument';
import { StatementReview } from '../widgets/DocumentReview/StatementReview';
import { documentPageTitle, documentSubtitle, optionStatusBadge, statusBadge } from '../widgets/DocumentReview/reviewView';
import { documentViewKind } from '../widgets/DocumentReview/viewKind';
import { type DocumentRow, buildDocumentRows } from '../widgets/FilesDrawer/documentRows';
type DocumentOption = { value: string | null; label: string; disabled: boolean; row: DocumentRow };
export function DocumentPage() {
const { document_id } = useParams();
const navigate = useNavigate();
const [doc, setDoc] = useState<DocumentOut | null>(null);
const [entityName, setEntityName] = useState('');
const [subtitle, setSubtitle] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [documentOptions, setDocumentOptions] = useState<DocumentOption[]>([]);
const [commentOpen, setCommentOpen] = useState(false);
const load = useCallback(async () => {
if (!document_id) return;
try {
const d = await getDocument(document_id);
setDoc(d);
setError(null);
fetchLegalEntity(d.legal_entity_id)
.then((le) => setEntityName(le.name))
.catch(() => setEntityName(''));
getFile(d.file_id)
.then((f) => {
setSubtitle(documentSubtitle(f));
// переключатель документа — файлы того же ЮЛ в той же консолидации, что и файл документа (без неё — все файлы ЮЛ)
listLegalEntityFiles(d.legal_entity_id, f.consolidation_id)
.then((files) =>
setDocumentOptions(
buildDocumentRows(files).map((row) => ({
value: row.documentId ?? row.fileId,
label: row.name,
disabled: !row.linkable,
row,
})),
),
)
.catch(() => setDocumentOptions([]));
})
.catch(() => setSubtitle(null));
} catch (e: unknown) {
setError(e instanceof ApiError ? e.message : 'Не удалось открыть документ');
}
}, [document_id]);
useEffect(() => {
void load();
}, [load]);
if (error) {
return <Result status='warning' title={error} extra={<Button onClick={() => navigate(-1)}>Назад</Button>} />;
}
if (!doc) {
return <Spin style={{ margin: 48 }} />;
}
const badge = statusBadge(doc.status);
const kind = documentViewKind(doc);
// Документ в статусе ERROR — данных формы/строк нет: показываем карточку и понятный текст, а не пустой экран.
const broken = doc.status === 'ERROR' && kind !== 'none';
return (
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', height: '100vh', gap: 12 }}>
<PageTitle
backable
location={['Отчётность', entityName, 'Проверка документа']}
header={documentPageTitle(doc.document_type)}
description={
subtitle ??
`${documentTypeLabel(doc.document_type, doc.osv_account_code)} · ${periodLabel(doc.period_year, doc.period_quarter, doc.is_cumulative)}`
}
extra={
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-end', width: 'min(520px, 48vw)' }}>
<Form style={{ flex: '1 1 auto', minWidth: 0 }} layout='vertical'>
<Form.Item style={{ marginBottom: 0 }} label='Документ'>
<Select<string, DocumentOption>
style={{ width: '100%' }}
value={doc.id}
options={documentOptions}
onChange={(value) => navigate(`/documents/${value}`)}
optionRender={(option) => (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
{/* у ББ и ОФР из одного PDF имя файла одно — различаются второй строкой (тип · формат · дата) */}
<Tooltip title={option.data.label}>
<div style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>
<div>{option.data.label}</div>
<div style={{ fontSize: 11, color: '#667085' }}>{option.data.row.subtitle}</div>
</div>
</Tooltip>
<TableTag {...optionStatusBadge(option.data.row.status)} />
</div>
)}
/>
</Form.Item>
</Form>
<TableTag text={badge.label} {...TAG_PRESETS[badge.color]} />
</div>
}
/>
<div style={{ alignSelf: 'flex-end' }}>
<Button icon={<CommentOutlined />} onClick={() => setCommentOpen(true)}>
Добавить комментарий
</Button>
</div>
<main
style={{
flex: '1 1 auto',
minHeight: 0,
background: kind === 'statement' ? 'transparent' : 'white',
borderRadius: 12,
padding: kind === 'statement' ? 0 : 12,
overflow: 'auto',
}}>
{broken && <Empty description='Документ не удалось распознать — данных для проверки нет' />}
{!broken && kind === 'osv' && <OsvDocument key={doc.id} document={doc} onChanged={load} />}
{!broken && kind === 'statement' && <StatementReview key={doc.id} document={doc} onChanged={load} />}
{kind === 'none' && <Empty description='Документ без данных для проверки' />}
</main>
<CommentModal open={commentOpen} documentId={doc.id} onClose={() => setCommentOpen(false)} />
</div>
);
}