Коммит 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>
140 lines
6.2 KiB
TypeScript
140 lines
6.2 KiB
TypeScript
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>
|
||
);
|
||
}
|