From 0242486eacf2e52f2abc1e6aeb00563a681a1d3f Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 19 Oct 2026 13:14:09 +0400 Subject: [PATCH] =?UTF-8?q?fix(#49):=20=D0=B7=D0=B5=D0=BB=D1=91=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20make=20check=20=D0=B8=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20(AURORA-144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - маршрут /documents/:document_id возвращён: старые ссылки («Открыть подтверждённую форму») снова открывают документ; ЮЛ и консолидация для переключателя документов берутся из документа и его файла, а не из адреса; в списке переключателя — вторая строка (тип · формат · дата), ББ и ОФР одного PDF различимы - чек-лист: ОФР — ячейка INCOME (было 'OFR' → всегда «Отсутствует»), сборка строк — buildChecklistRows с тестами, rowKey по ЮЛ, типы ответа GET …/checklist - удаление файла по id файла (fileId строки), а не по id документа (было 404); ошибка — без закрытия окна - замена: настоящая загрузка нового файла в ту же консолидацию (автозамена ядра, 05-domain-rules §9.5 п. 3), причина — комментарием к заменяемому документу; кнопка доступна только в версии в работе - комментарий: путь без хвостового «/», типы, закрытие только после успеха, лимит 4000 - счётчики над таблицей формы — модуль reviewCounters (по needs_review, как подсветка) с тестами; buildStatementGrid возвращён к test (падали 3 теста) - PageTitle и store возвращены к test (ширина переключателя — внутри DocumentPage; имя ЮЛ — пропсом) - цвет подписей полей в теме: #667085 (опечатка #665087) - типы во всех новых компонентах: tsc 0 ошибок, ESLint/biome без ошибок, vitest 331/331, сборка Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 3 +- src/App.tsx | 2 +- src/components/PageTitle.tsx | 24 +-- src/index.tsx | 2 +- src/layouts/DocumentPage.tsx | 156 +++++++----------- src/services/api/consolidations.ts | 4 +- src/services/api/documents.ts | 6 +- src/services/api/files.ts | 2 +- .../consolidations/consolidationsSlice.ts | 8 +- src/store/slices/consolidations/types.ts | 1 - src/types/consolidations/version_members.ts | 2 +- .../ConsoleLegalEntities/ChecklistModal.tsx | 60 ++----- .../ConsoleLegalEntities.tsx | 11 +- .../checklist_utils.test.ts | 39 +++++ .../ConsoleLegalEntities/checklist_utils.tsx | 135 +++++++++------ src/widgets/DocumentReview/CommentModal.tsx | 45 +++-- .../DocumentReview/StatementReview.tsx | 10 +- .../DocumentReview/reviewCounters.test.ts | 67 ++++++++ src/widgets/DocumentReview/reviewCounters.ts | 31 ++++ src/widgets/DocumentReview/reviewView.test.ts | 20 ++- src/widgets/DocumentReview/reviewView.ts | 23 ++- src/widgets/DocumentReview/statementGrid.ts | 36 +--- src/widgets/FilesDrawer/DeleteModal.tsx | 49 +++--- src/widgets/FilesDrawer/FilesDrawer.tsx | 40 +++-- src/widgets/FilesDrawer/FilesList.tsx | 13 +- src/widgets/FilesDrawer/ReplaceDocModal.tsx | 54 +++--- src/widgets/FilesDrawer/documentRows.test.ts | 7 + src/widgets/FilesDrawer/documentRows.ts | 11 +- 28 files changed, 479 insertions(+), 382 deletions(-) create mode 100644 src/widgets/ConsoleLegalEntities/checklist_utils.test.ts create mode 100644 src/widgets/DocumentReview/reviewCounters.test.ts create mode 100644 src/widgets/DocumentReview/reviewCounters.ts diff --git a/CLAUDE.md b/CLAUDE.md index bbe34c0..4c404f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,8 @@ SPA кредитного аналитика АРМ КА: справочники список для панели и `collapseIssues`; `statementDraft` — весь черновик правки и сборка тела `PATCH` (суммы, подтверждения, коды, даты, шапка); `cellView` — цвета, подсказки, текст ячейки суммы и кода; `codeSuggestions` — подсказки кодов по справочнику; `lineMenu` — меню строки («Это расшифровка», «Назначить код»); `editRules` — что можно править и какая кнопка подтверждения нужна ячейке/колонке; - `confirmBlockers` — строки без кода, блокирующие подтверждение; `focusTarget` — к какой строке прокрутить по «Показать». + `confirmBlockers` — строки без кода, блокирующие подтверждение; `focusTarget` — к какой строке прокрутить по «Показать»; + `reviewCounters` — счётчики «изменённых» и «требующих проверки» над таблицей (по `needs_review`, как подсветка). `src/widgets/FilesDrawer/fileWarnings` и `documentRows` — предупреждения файла и документа в панели файлов; `src/utils/percent` — формат уверенности. diff --git a/src/App.tsx b/src/App.tsx index 736da23..24faf7a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,7 +43,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> diff --git a/src/components/PageTitle.tsx b/src/components/PageTitle.tsx index a7f5688..c79a8cf 100644 --- a/src/components/PageTitle.tsx +++ b/src/components/PageTitle.tsx @@ -19,16 +19,8 @@ const crumbs = (location: string | string[]) => export default function PageTitle({ location, header, description, backable = false, extra }: PageTitleProps) { const navigate = useNavigate(); return ( -
- {/*
*/} -
+
+
{backable && (
- {extra && ( -
- {extra} -
- )} + {extra &&
{extra}
}
); } diff --git a/src/index.tsx b/src/index.tsx index dd1f6db..83b74ef 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -50,7 +50,7 @@ createRoot(rootElement).render( }, Form: { labelFontSize: 12, - labelColor: '#665087', + labelColor: '#667085', }, }, }}> diff --git a/src/layouts/DocumentPage.tsx b/src/layouts/DocumentPage.tsx index 10f4e41..661a6d1 100644 --- a/src/layouts/DocumentPage.tsx +++ b/src/layouts/DocumentPage.tsx @@ -1,63 +1,66 @@ -import { Button, Empty, Form, message, Result, Select, Spin, Tooltip } from 'antd'; +import { CommentOutlined } from '@ant-design/icons'; +import { Button, Empty, Form, Result, Select, Spin, Tooltip } from 'antd'; import { useCallback, useEffect, useState } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +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 { buildDocumentRows } from '../widgets/FilesDrawer/documentRows'; -import { TableTag } from '../components/TableTag/TableTag'; -import { TAG_PRESETS } from '../components/TableTag/presets'; -import { CommentOutlined } from '@ant-design/icons'; -import CommentModal from '../widgets/DocumentReview/CommentModal'; +import { type DocumentRow, buildDocumentRows } from '../widgets/FilesDrawer/documentRows'; + +type DocumentOption = { value: string | null; label: string; disabled: boolean; row: DocumentRow }; export function DocumentPage() { - const [searchParams, setSearchParams] = useSearchParams(); - const document_id = searchParams.get('document_id'); - const legal_entity_id = searchParams.get('legal_entity_id'); - const consolidation_id = searchParams.get('consolidation_id'); + const { document_id } = useParams(); const navigate = useNavigate(); const [doc, setDoc] = useState(null); const [entityName, setEntityName] = useState(''); const [subtitle, setSubtitle] = useState(null); const [error, setError] = useState(null); - const [filesOptions, setFilesOptions] = useState([]); + const [documentOptions, setDocumentOptions] = useState([]); const [commentOpen, setCommentOpen] = useState(false); const load = useCallback(async () => { if (!document_id) return; try { const d = await getDocument(document_id); - const filesJson = await listLegalEntityFiles(legal_entity_id, consolidation_id); - const documentOptions = buildDocumentRows(filesJson); - - const options = documentOptions.map((d) => ({ - disabled: d.documentId === null, - label: d.name, - value: d.documentId, - ...d, - title: '', - })); - setFilesOptions(options); 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))) + .then((f) => { + setSubtitle(documentSubtitle(f)); + // переключатель документа — файлы того же ЮЛ в той же консолидации, что и файл документа (без неё — все файлы ЮЛ) + listLegalEntityFiles(d.legal_entity_id, f.consolidation_id) + .then((files) => + setDocumentOptions( + buildDocumentRows(files).map((row) => ({ + value: row.documentId, + label: row.name, + disabled: !row.linkable, + row, + })), + ), + ) + .catch(() => setDocumentOptions([])); + }) .catch(() => setSubtitle(null)); } catch (e: unknown) { setError(e instanceof ApiError ? e.message : 'Не удалось открыть документ'); } - }, [document_id, legal_entity_id, consolidation_id]); + }, [document_id]); useEffect(() => { void load(); @@ -74,14 +77,7 @@ export function DocumentPage() { // Документ в статусе ERROR — данных формы/строк нет: показываем карточку и понятный текст, а не пустой экран. const broken = doc.status === 'ERROR' && kind !== 'none'; return ( -
+
-
- - {document_id && ( - - )} + + +
+ )} + /> -
- -
+
} /> - {true && ( -
- -
- )} +
+ +
} {kind === 'none' && }
- { - setCommentOpen(false); - }} - /> + setCommentOpen(false)} />
); } diff --git a/src/services/api/consolidations.ts b/src/services/api/consolidations.ts index e20dcf8..28e1220 100644 --- a/src/services/api/consolidations.ts +++ b/src/services/api/consolidations.ts @@ -2,12 +2,12 @@ import { api, apiFetchBlob } from '../../api/client'; import type { ConsolidationCreate } from '../../types/api'; import type { LockOut } from '../../types/consolidations/common_types'; import type { IConsolidation } from '../../types/consolidations/consolidations'; -import type { VersionCard } from '../../types/consolidations/version_members'; +import type { ChecklistBackendShape, VersionCard } from '../../types/consolidations/version_members'; export const getConsolidations = (groupId: string) => api.get(`/company-groups/${groupId}/consolidations`); export const getVersionCard = (versionId: string) => api.get(`/consolidations/versions/${versionId}`); export const createConsolidation = (body: ConsolidationCreate) => api.post('/consolidations', body); export const downloadChecklistCsv = (versionId: string) => apiFetchBlob(`/consolidations/versions/${versionId}/checklist?format=csv`); -export const getChecklist = (versionId: string) => api.get(`/consolidations/versions/${versionId}/checklist`); +export const getChecklist = (versionId: string) => api.get(`/consolidations/versions/${versionId}/checklist`); /** «Взять в работу»: 201 — взята, 200 — уже ваша; чужая — 409 `LOCKED_BY_OTHER` (перехват — с `takeover_reason`). */ export const lockVersion = (versionId: string) => api.post(`/consolidations/versions/${versionId}/lock`); diff --git a/src/services/api/documents.ts b/src/services/api/documents.ts index 30de97c..64bd52d 100644 --- a/src/services/api/documents.ts +++ b/src/services/api/documents.ts @@ -22,7 +22,5 @@ export async function getAllEntries(id: string): Promise { } } -export function postComment(document_id: string, comment: string) { - const body = { text: comment }; - return api.post(`/documents/${document_id}/comments/`, body); -} +/** Комментарий к документу: текст 1–4000 символов после обрезки пробелов (иначе 422). */ +export const postComment = (documentId: string, text: string) => api.post(`/documents/${documentId}/comments`, { text }); diff --git a/src/services/api/files.ts b/src/services/api/files.ts index 5d0b9d2..9b419b0 100644 --- a/src/services/api/files.ts +++ b/src/services/api/files.ts @@ -1,4 +1,4 @@ -import { api, ApiError, apiFetch, apiFetchBlob } from '../../api/client'; +import { ApiError, api, apiFetch, apiFetchBlob } from '../../api/client'; import type { FileOut, Page, UploadResult } from '../../types/api'; export function isUploadResult(body: unknown): body is UploadResult { diff --git a/src/store/slices/consolidations/consolidationsSlice.ts b/src/store/slices/consolidations/consolidationsSlice.ts index 4f873c2..1beef35 100644 --- a/src/store/slices/consolidations/consolidationsSlice.ts +++ b/src/store/slices/consolidations/consolidationsSlice.ts @@ -6,7 +6,6 @@ const initialState: ConsolidationsSliceType = { consolidations: [], current_version_id: null, current_legal_entity_id: null, - current_legal_entity_name: null, loading: false, error: null, }; @@ -18,12 +17,9 @@ const consolidationsSlice = createSlice({ setSelectedVersion: (state, action: PayloadAction) => { state.current_version_id = action.payload; }, - setSelectedLegalEntityId: (state, action: PayloadAction) => { + setSelectedLegalEntity: (state, action: PayloadAction) => { state.current_legal_entity_id = action.payload; }, - setSelectedLegalEntityName: (state, action: PayloadAction) => { - state.current_legal_entity_name = action.payload; - }, }, extraReducers: (builder) => { builder @@ -43,4 +39,4 @@ const consolidationsSlice = createSlice({ }); export default consolidationsSlice.reducer; -export const { setSelectedVersion, setSelectedLegalEntityId, setSelectedLegalEntityName } = consolidationsSlice.actions; +export const { setSelectedVersion, setSelectedLegalEntity } = consolidationsSlice.actions; diff --git a/src/store/slices/consolidations/types.ts b/src/store/slices/consolidations/types.ts index ab6a346..3a4ef02 100644 --- a/src/store/slices/consolidations/types.ts +++ b/src/store/slices/consolidations/types.ts @@ -6,5 +6,4 @@ export type ConsolidationsSliceType = { current_version_id: string | null; error: string | null; current_legal_entity_id: string | null; - current_legal_entity_name: string | null; }; diff --git a/src/types/consolidations/version_members.ts b/src/types/consolidations/version_members.ts index 18a559a..a8212ac 100644 --- a/src/types/consolidations/version_members.ts +++ b/src/types/consolidations/version_members.ts @@ -44,7 +44,7 @@ type ChecklistRow = { type ChecklistRule = { document_type: DocumentType; - account: string; + account: string | null; is_mandatory: boolean; }; diff --git a/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx b/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx index 63724c6..43e0db7 100644 --- a/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx +++ b/src/widgets/ConsoleLegalEntities/ChecklistModal.tsx @@ -1,10 +1,10 @@ -import { useEffect, useMemo, useState } from 'react'; -import { getChecklist } from '../../services/api/consolidations'; -import { message, Modal, Table, TableColumnsType } from 'antd'; -import { ChecklistBackendShape, type IVersionCard } from '../../types/consolidations/version_members'; import { DownloadOutlined } from '@ant-design/icons'; -import { buildChecklistColumns, getMarksItems } from './checklist_utils'; -// import { useMe } from '../../auth/useMe'; +import { Modal, Table, message } from 'antd'; +import { useEffect, useMemo, useState } from 'react'; +import { ApiError } from '../../api/client'; +import { getChecklist } from '../../services/api/consolidations'; +import type { ChecklistBackendShape, IVersionCard } from '../../types/consolidations/version_members'; +import { buildChecklistColumns, buildChecklistRows, getMarksItems } from './checklist_utils'; type ChecklistProps = { open: boolean; @@ -16,48 +16,16 @@ type ChecklistProps = { export default function ChecklistModal({ open, onDownload, consolidationData, onClose }: ChecklistProps) { const [checklistData, setChecklistData] = useState(null); - const displayedData = useMemo(() => { - if (!checklistData) return; - const result = []; - for (const row of checklistData.rows) { - const rowObj = { - name: row.legal_entity_name, - inn: row.legal_entity_inn, - ofr: 'MISSING', - balance: 'MISSING', - }; - for (const cell of row.cells) { - if (cell.document_type === 'OSV') { - const code = cell.account; - const key = `osv_${code}`; - rowObj[key] = cell.status; - } else if (cell.document_type === 'BALANCE') { - rowObj.balance = cell.status; - } else if (cell.document_type === 'OFR') { - rowObj.ofr = cell.status; - } - } - result.push(rowObj); - } - return result; - }, [checklistData]); - const columns: TableColumnsType = useMemo(() => { - if (!checklistData) return []; - return buildChecklistColumns(checklistData); - }, [checklistData]); + const rows = useMemo(() => (checklistData ? buildChecklistRows(checklistData) : []), [checklistData]); + const columns = useMemo(() => (checklistData ? buildChecklistColumns(checklistData) : []), [checklistData]); useEffect(() => { - async function fetchChecklist() { - if (!consolidationData.id || !open) return; - try { - const res = await getChecklist(consolidationData.id); - setChecklistData(res); - } catch (e) { - message.error(e.message); - } - } - fetchChecklist(); + if (!consolidationData.id || !open) return; + getChecklist(consolidationData.id) + .then(setChecklistData) + .catch((e: unknown) => message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить чек-лист')); }, [open, consolidationData.id]); + return ( }}>
{getMarksItems()} - +
); diff --git a/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx b/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx index bc59727..44aa8fa 100644 --- a/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx +++ b/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx @@ -6,18 +6,14 @@ import { ColumnSettings, DataTable, useFilteredRows, useVisibleColumns } from '. import { useTableFilters } from '../../components/GenericTable/useTableFilters'; import { downloadChecklistCsv } from '../../services/api/consolidations'; import { getVersionCard } from '../../services/api/consolidations'; -import { - setSelectedLegalEntityId, - setSelectedLegalEntityName, - setSelectedVersion, -} from '../../store/slices/consolidations/consolidationsSlice'; +import { setSelectedLegalEntity, setSelectedVersion } from '../../store/slices/consolidations/consolidationsSlice'; import { useAppDispatch, useAppSelector } from '../../store/store'; import type { IVersionCard, MemberRow } from '../../types/consolidations/version_members'; import { saveBlob } from '../../utils/download'; +import ChecklistModal from './ChecklistModal'; import { membersConfig } from './config'; import { normalizeData } from './normalize'; import { checklistFilename, responsibleName } from './panelInfo'; -import ChecklistModal from './ChecklistModal'; export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawerOpen: () => void; reloadKey: number }) { const dispatch = useAppDispatch(); @@ -64,8 +60,7 @@ export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawe const onRow = (row: MemberRow) => ({ onClick: () => { dispatch(setSelectedVersion(row.version_id)); - dispatch(setSelectedLegalEntityId(row.id)); - dispatch(setSelectedLegalEntityName(row.name)); + dispatch(setSelectedLegalEntity(row.id)); onDrawerOpen(); }, }); diff --git a/src/widgets/ConsoleLegalEntities/checklist_utils.test.ts b/src/widgets/ConsoleLegalEntities/checklist_utils.test.ts new file mode 100644 index 0000000..fc67fd6 --- /dev/null +++ b/src/widgets/ConsoleLegalEntities/checklist_utils.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import type { ChecklistBackendShape } from '../../types/consolidations/version_members'; +import { buildChecklistRows } from './checklist_utils'; + +const checklist: ChecklistBackendShape = { + rules: [], + rows: [ + { + legal_entity_id: 'le1', + legal_entity_name: 'РУССКИЙ БИСКВИТ, АО', + legal_entity_inn: '3528052718', + cells: [ + { document_type: 'BALANCE', account: null, status: 'NEEDS_CHECK', missing_quarters: [], document_ids: ['d1'] }, + { document_type: 'INCOME', account: null, status: 'CONFIRMED', missing_quarters: [], document_ids: ['d2'] }, + { document_type: 'OSV', account: '60', status: 'ERROR', missing_quarters: [], document_ids: ['d3'] }, + ], + }, + ], +}; + +describe('buildChecklistRows', () => { + it('ОФР — ячейка INCOME, ББ — BALANCE, ОСВ — по счёту; ключ строки — ЮЛ', () => { + expect(buildChecklistRows(checklist)).toEqual([ + { + key: 'le1', + name: 'РУССКИЙ БИСКВИТ, АО', + inn: '3528052718', + balance: 'NEEDS_CHECK', + ofr: 'CONFIRMED', + osv: { '60': 'ERROR' }, + }, + ]); + }); + + it('нет ячеек ББ/ОФР — «Отсутствует»', () => { + const rows = buildChecklistRows({ rules: [], rows: [{ ...checklist.rows[0], cells: [] }] }); + expect(rows[0]).toMatchObject({ balance: 'MISSING', ofr: 'MISSING', osv: {} }); + }); +}); diff --git a/src/widgets/ConsoleLegalEntities/checklist_utils.tsx b/src/widgets/ConsoleLegalEntities/checklist_utils.tsx index 2314758..606d72b 100644 --- a/src/widgets/ConsoleLegalEntities/checklist_utils.tsx +++ b/src/widgets/ConsoleLegalEntities/checklist_utils.tsx @@ -6,34 +6,47 @@ import { EyeOutlined, MinusCircleOutlined, } from '@ant-design/icons'; -import { TAG_PRESETS } from '../../components/TableTag/presets'; -import { ChecklistBackendShape, FileStatus } from '../../types/consolidations/version_members'; -import { Avatar, TableColumnsType } from 'antd'; +import { Avatar, type TableColumnsType } from 'antd'; +import type { ReactNode } from 'react'; +import { type Preset, TAG_PRESETS } from '../../components/TableTag/presets'; +import type { ChecklistBackendShape, FileStatus } from '../../types/consolidations/version_members'; import { statusMapping } from './consts'; -export const mapStatus = (status: FileStatus) => { - const presetMapping = { - MISSING: TAG_PRESETS['default'], - CONFIRMED: TAG_PRESETS['green'], - NEEDS_CHECK: TAG_PRESETS['orange'], - RECOGNIZED: TAG_PRESETS['gold'], - ERROR: TAG_PRESETS['red'], - PROCESSING: TAG_PRESETS['geekblue'], - }; - const preset = presetMapping[status]; +/** Строка таблицы чек-листа: ЮЛ и статусы ББ, ОФР и ОСВ по счетам. */ +export type ChecklistTableRow = { + key: string; + name: string; + inn: string; + balance: FileStatus; + ofr: FileStatus; + osv: Record; +}; - const iconMapping = { - MISSING: , - NEEDS_CHECK: , - ERROR: , - CONFIRMED: , - RECOGNIZED: , - PROCESSING: , - }; +const presetMapping: Record = { + MISSING: TAG_PRESETS.default, + CONFIRMED: TAG_PRESETS.green, + NEEDS_CHECK: TAG_PRESETS.orange, + RECOGNIZED: TAG_PRESETS.gold, + ERROR: TAG_PRESETS.red, + PROCESSING: TAG_PRESETS.geekblue, +}; + +const iconMapping: Record = { + MISSING: , + NEEDS_CHECK: , + ERROR: , + CONFIRMED: , + RECOGNIZED: , + PROCESSING: , +}; + +export const mapStatus = (status: FileStatus) => { + const preset = presetMapping[status]; return ( ); }; + export function getMarksItems() { return (
Условные обозначения - {Object.entries(statusMapping).map((item) => { - const avatar = mapStatus(item[0] as FileStatus); - return ( -
- {avatar} {item[1]} -
- ); - })} + {(Object.entries(statusMapping) as [FileStatus, string][]).map(([status, label]) => ( +
+ {mapStatus(status)} {label} +
+ ))}
); } -export function buildChecklistColumns(checklistData: ChecklistBackendShape) { - const osv_children = []; - const osv_codes = new Set(); + +/** Строки таблицы из ответа `GET …/checklist`: ОФР — ячейка `INCOME`; нет ячейки ББ/ОФР — «Отсутствует». */ +export function buildChecklistRows(checklistData: ChecklistBackendShape): ChecklistTableRow[] { + return checklistData.rows.map((row) => { + const result: ChecklistTableRow = { + key: row.legal_entity_id, + name: row.legal_entity_name, + inn: row.legal_entity_inn, + balance: 'MISSING', + ofr: 'MISSING', + osv: {}, + }; + for (const cell of row.cells) { + if (cell.document_type === 'OSV' && cell.account) { + result.osv[cell.account] = cell.status; + } else if (cell.document_type === 'BALANCE') { + result.balance = cell.status; + } else if (cell.document_type === 'INCOME') { + result.ofr = cell.status; + } + } + return result; + }); +} + +export function buildChecklistColumns(checklistData: ChecklistBackendShape): TableColumnsType { + const osvCodes: string[] = []; for (const row of checklistData.rows) { for (const cell of row.cells) { - if (cell.document_type === 'OSV') { - const code = cell.account; - if (!osv_codes.has(code)) { - osv_children.push({ key: `osv_${code}`, dataIndex: `osv_${code}`, title: code, render: (v) => mapStatus(v) }); - osv_codes.add(code); - } + if (cell.document_type === 'OSV' && cell.account && !osvCodes.includes(cell.account)) { + osvCodes.push(cell.account); } } } - const result: TableColumnsType = [ + return [ { key: 'name', - dataIndex: 'leName', title: 'ОРГАНИЗАЦИЯ', - render: (v, r) => ( -
-
- {r.name}: {r.inn} -
-
+ render: (_, r) => ( +
+ {r.name}: {r.inn} +
), }, - { key: 'balance', dataIndex: 'balance', title: 'БУХ.БАЛАНС', render: (v) => mapStatus(v) }, - { key: 'ofr', dataIndex: 'ofr', title: 'ОФР', render: (v) => mapStatus(v) }, - { title: 'ОСВ', children: osv_children, render: (v) => mapStatus(v) }, + { key: 'balance', dataIndex: 'balance', title: 'БУХ.БАЛАНС', render: (v: FileStatus) => mapStatus(v) }, + { key: 'ofr', dataIndex: 'ofr', title: 'ОФР', render: (v: FileStatus) => mapStatus(v) }, + { + key: 'osv', + title: 'ОСВ', + children: osvCodes.map((code) => ({ + key: `osv_${code}`, + title: code, + render: (_: unknown, r: ChecklistTableRow) => mapStatus(r.osv[code] ?? 'MISSING'), + })), + }, ]; - - return result; } diff --git a/src/widgets/DocumentReview/CommentModal.tsx b/src/widgets/DocumentReview/CommentModal.tsx index 48c5af5..bfc491c 100644 --- a/src/widgets/DocumentReview/CommentModal.tsx +++ b/src/widgets/DocumentReview/CommentModal.tsx @@ -1,41 +1,50 @@ -import { Form, Input, message, Modal } from 'antd'; +import { Form, Input, Modal, message } from 'antd'; import { useState } from 'react'; +import { ApiError } from '../../api/client'; import { postComment } from '../../services/api/documents'; -export default function CommentModal({ - document_id, // () => void - open, - onClose, -}) { +type CommentModalProps = { documentId: string; open: boolean; onClose: () => void }; + +export default function CommentModal({ documentId, open, onClose }: CommentModalProps) { const [comment, setComment] = useState(''); - const [isLoading, setIsloading] = useState(false); + const [saving, setSaving] = useState(false); + const close = () => { + setComment(''); + onClose(); + }; const handleOk = async () => { - setIsloading(true); + setSaving(true); try { - const res = await postComment(document_id, comment); - } catch (e) { - message.error(e.message); + await postComment(documentId, comment.trim()); + message.success('Комментарий добавлен'); + close(); + } catch (e: unknown) { + // окно не закрываем: текст не теряется, можно повторить + message.error(e instanceof ApiError ? e.message : 'Не удалось добавить комментарий'); } finally { - setIsloading(false); - onClose(); + setSaving(false); } }; return ( Добавить комментарий} + title='Добавить комментарий' okText='Добавить' onOk={handleOk} - okButtonProps={{ disabled: comment.length === 0 }} + okButtonProps={{ disabled: comment.trim().length === 0 }} + confirmLoading={saving} open={open} cancelText='Отмена' - onCancel={onClose} + onCancel={close} destroyOnHidden>
Комментарий}> - setComment(e.target.value)} - placeholder='Опишите результат проверки или внесенные правки'> + autoSize={{ minRows: 3 }} + maxLength={4000} + placeholder='Опишите результат проверки или внесенные правки' + />
diff --git a/src/widgets/DocumentReview/StatementReview.tsx b/src/widgets/DocumentReview/StatementReview.tsx index 893e21a..349b107 100644 --- a/src/widgets/DocumentReview/StatementReview.tsx +++ b/src/widgets/DocumentReview/StatementReview.tsx @@ -20,6 +20,7 @@ import { switchesFor } from './focusTarget'; import { detailCount, filterLayout, formLayout } from './formLayout'; import { issueListItems, issuePlaces } from './issuePlaces'; import { applyLineMenu, showsCodeInput } from './lineMenu'; +import { countReviewValues } from './reviewCounters'; import { reviewActions } from './reviewView'; import { type DraftError, type StatementDraft, buildStatementPatch, emptyDraft, isDraftEmpty } from './statementDraft'; import { applyDraftInput, buildStatementGrid, emptySublineKeys } from './statementGrid'; @@ -68,6 +69,11 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut }, [load]); const grid = useMemo(() => (statement ? buildStatementGrid(statement) : null), [statement]); + // счётчики над таблицей — только пока форма на проверке + const counters = useMemo( + () => (statement && statement.document.status === 'NEEDS_CHECK' ? countReviewValues(statement) : null), + [statement], + ); // пустые строки детализации скрыты по умолчанию (только показ: PATCH и черновик работают по всем строкам формы) const emptySublines = useMemo(() => (grid ? emptySublineKeys(grid.rows, draft.amounts, draft.lines) : new Set()), [grid, draft]); const fullLayout = useMemo(() => (grid && statement ? formLayout(statement.document.document_type, grid.rows) : []), [grid, statement]); @@ -295,9 +301,9 @@ export function StatementReview({ document, onChanged }: { document: DocumentOut )} {serverErrorOffScreen && } - {grid.changedValues !== null && ( + {counters && (

- Измененных значений: {grid.changedValues}. Значений, требующих проверки: {grid.unrecognizedValues} + Изменённых значений: {counters.changed}. Значений, требующих проверки: {counters.needsReview}

)} = {}): ValueOut => ({ + id: 'v', + column_id: 'c1', + column_index: 0, + amount: '1.00', + string_value: null, + text: null, + confidence: '100', + is_recognized: true, + is_modified: false, + original_amount: null, + needs_review: false, + is_confirmed: false, + ...extra, +}); + +const field = (extra: Partial = {}): HeaderFieldOut => ({ + value: 'x', + original_value: 'x', + verified: null, + modified: null, + ...extra, +}); + +const statement = (values: ValueOut[], header: Partial = {}) => + ({ + header: { + inn: field(), + report_date: field(), + units: field(), + organization_name_modified: false, + needs_review_fields: [], + ...header, + }, + lines: [{ id: 'l1', values }], + }) as unknown as StatementOut; + +describe('countReviewValues', () => { + it('чистая форма — нули', () => { + expect(countReviewValues(statement([v(), v()]))).toEqual({ changed: 0, needsReview: 0 }); + }); + + it('на проверку — needs_review без подтверждения и без правки (как подсветка ячейки)', () => { + const values = [ + v({ needs_review: true }), + v({ needs_review: true, is_confirmed: true }), + v({ needs_review: true, is_modified: true }), + // достроенная сервером ячейка: is_recognized = null, на проверку не считается + v({ id: null, is_recognized: null, amount: null }), + ]; + expect(countReviewValues(statement(values))).toEqual({ changed: 1, needsReview: 1 }); + }); + + it('шапка: правки полей и наименования, поля из needs_review_fields без подтверждения', () => { + const s = statement([], { + inn: field({ modified: true }), + report_date: field({ verified: true }), + organization_name_modified: true, + needs_review_fields: ['report_date', 'units'], + }); + expect(countReviewValues(s)).toEqual({ changed: 2, needsReview: 1 }); + }); +}); diff --git a/src/widgets/DocumentReview/reviewCounters.ts b/src/widgets/DocumentReview/reviewCounters.ts new file mode 100644 index 0000000..7dfb197 --- /dev/null +++ b/src/widgets/DocumentReview/reviewCounters.ts @@ -0,0 +1,31 @@ +import type { HeaderFieldOut, StatementOut } from '../../types/api'; + +const HEADER_FIELDS = ['inn', 'report_date', 'units'] as const; + +/** + * Счётчики над таблицей формы: «изменённых» — правки аналитика (ячейки, поля шапки, наименование организации); + * «требующих проверки» — то же, что подсвечено на экране: ячейка с `needs_review` и поле из `needs_review_fields` + * (решает ядро), ещё не подтверждённые и не исправленные. Достроенные сервером пустые ячейки не считаются. + */ +export function countReviewValues(statement: StatementOut): { changed: number; needsReview: number } { + let changed = 0; + let needsReview = 0; + for (const line of statement.lines) { + for (const c of line.values) { + if (c.is_modified) changed++; + else if (c.needs_review && !c.is_confirmed) needsReview++; + } + } + const header = statement.header; + for (const key of HEADER_FIELDS) { + if (header[key].modified) changed++; + } + if (header.organization_name_modified) changed++; + for (const name of header.needs_review_fields ?? []) { + const field: HeaderFieldOut | undefined = (HEADER_FIELDS as readonly string[]).includes(name) + ? header[name as (typeof HEADER_FIELDS)[number]] + : undefined; + if (!field?.modified && !field?.verified) needsReview++; + } + return { changed, needsReview }; +} diff --git a/src/widgets/DocumentReview/reviewView.test.ts b/src/widgets/DocumentReview/reviewView.test.ts index 108cae4..8ec4a94 100644 --- a/src/widgets/DocumentReview/reviewView.test.ts +++ b/src/widgets/DocumentReview/reviewView.test.ts @@ -1,5 +1,14 @@ import { describe, expect, it } from 'vitest'; -import { columnTitle, documentPageTitle, documentSubtitle, formTitle, formatReportDate, reviewActions, statusBadge } from './reviewView'; +import { + columnTitle, + documentPageTitle, + documentSubtitle, + formTitle, + formatReportDate, + optionStatusBadge, + reviewActions, + statusBadge, +} from './reviewView'; describe('formatReportDate', () => { it('ISO → дд.мм.гггг', () => { @@ -69,3 +78,12 @@ describe('columnTitle', () => { expect(columnTitle(col(null, null))).toBe('Колонка 2'); }); }); + +describe('optionStatusBadge', () => { + it('текст значка по статусу строки документа', () => { + expect(optionStatusBadge('success').text).toBe('Загружен'); + expect(optionStatusBadge('warning').text).toBe('Требуется проверка'); + expect(optionStatusBadge('processing').text).toBe('Обработка'); + expect(optionStatusBadge('error').text).toBe('Ошибка'); + }); +}); diff --git a/src/widgets/DocumentReview/reviewView.ts b/src/widgets/DocumentReview/reviewView.ts index d499e00..9007893 100644 --- a/src/widgets/DocumentReview/reviewView.ts +++ b/src/widgets/DocumentReview/reviewView.ts @@ -1,8 +1,8 @@ -import { Preset, TAG_PRESETS } from '../../components/TableTag/presets'; +import { type Preset, TAG_PRESETS } from '../../components/TableTag/presets'; import type { ColumnOut, DocumentStatus, DocumentType, FileOut } from '../../types/api'; import { formatMoscowDate } from '../../utils/dates'; import { documentStatusLabel } from '../../utils/labels'; -import { RowStatus } from '../FilesDrawer/documentRows'; +import type { RowStatus } from '../FilesDrawer/documentRows'; /** '2025-09-30' (или ISO с временем) → '30.09.2025'; пусто → null; нераспознанное — как есть. */ export function formatReportDate(value: string | null | undefined): string | null { @@ -34,17 +34,16 @@ export function statusBadge(status: DocumentStatus): { label: string; color: 'gr } return { label, color: status === 'ERROR' ? 'red' : 'orange' }; } +const OPTION_BADGES: Record = { + success: { text: 'Загружен', ...TAG_PRESETS.green }, + warning: { text: 'Требуется проверка', ...TAG_PRESETS.orange }, + processing: { text: 'Обработка', ...TAG_PRESETS.geekblue }, + error: { text: 'Ошибка', ...TAG_PRESETS.red }, +}; + +/** Значок статуса документа в переключателе документов на экране проверки. */ export function optionStatusBadge(status: RowStatus): { text: string } & Preset { - if (status === 'success') { - const preset = TAG_PRESETS.green; - const text = 'Загружен'; - return { text, ...preset }; - } else if (status === 'warning') { - return { text: 'Требуется проверка', ...TAG_PRESETS.orange }; - } else if (status === 'processing') { - return { text: 'Обработка', ...TAG_PRESETS.geekblue }; - } - return { text: 'Ошибка', ...TAG_PRESETS.red }; + return OPTION_BADGES[status]; } export function documentSubtitle(file: Pick): string { diff --git a/src/widgets/DocumentReview/statementGrid.ts b/src/widgets/DocumentReview/statementGrid.ts index 4c232f3..a80c53d 100644 --- a/src/widgets/DocumentReview/statementGrid.ts +++ b/src/widgets/DocumentReview/statementGrid.ts @@ -28,39 +28,7 @@ export type GridRow = { /** Ключ строки: id из БД, у достроенной строки (id = null) — код; двоеточия в ключе нет (разделитель cellKey). */ const rowKey = (id: string | null, code: string | null) => id ?? `code-${code ?? ''}`; -export function buildStatementGrid(statement: StatementOut): { - columns: ColumnOut[]; - rows: GridRow[]; - changedValues: number | null; - unrecognizedValues: number | null; -} { - let changedValues: number | null = 0; - let unrecognizedValues: number | null = 0; - if (statement.document.status === 'NEEDS_CHECK') { - for (const key of ['inn', 'report_date', 'units']) { - const val = statement.header[key]; - if (!val['verified'] && !val['modified']) { - unrecognizedValues++; - } - if (val['modified']) { - changedValues++; - } - } - for (const l of statement.lines) { - for (const c of l.values) { - if (!c.is_recognized && !c.is_modified) { - unrecognizedValues++; - } - if (c.is_modified) { - changedValues++; - } - } - } - } else { - unrecognizedValues = null; - changedValues = null; - } - +export function buildStatementGrid(statement: StatementOut): { columns: ColumnOut[]; rows: GridRow[] } { const columns = [...statement.columns].sort((a, b) => a.column_index - b.column_index); const rows = statement.lines.map((line) => { const cells: Record = {}; @@ -86,7 +54,7 @@ export function buildStatementGrid(statement: StatementOut): { cells, }; }); - return { columns, rows, changedValues, unrecognizedValues }; + return { columns, rows }; } /** diff --git a/src/widgets/FilesDrawer/DeleteModal.tsx b/src/widgets/FilesDrawer/DeleteModal.tsx index 1d81742..c01ac60 100644 --- a/src/widgets/FilesDrawer/DeleteModal.tsx +++ b/src/widgets/FilesDrawer/DeleteModal.tsx @@ -1,10 +1,12 @@ -import React, { useCallback, useState } from 'react'; -import { Modal, Button, Typography, theme, message } from 'antd'; +import { Button, Modal, Tooltip, Typography, message, theme } from 'antd'; +import { type ReactNode, useState } from 'react'; +import { ApiError } from '../../api/client'; import { removeFile } from '../../services/api/files'; +import type { DocumentRow } from './documentRows'; const { Text } = Typography; -const OptionCard = ({ title, detail, action }) => { +const OptionCard = ({ title, detail, action }: { title: string; detail: string; action: ReactNode }) => { const { token } = theme.useToken(); return (
{ ); }; -export default function DeleteReasonModal({ - targetRow, - open, - onCancel, - onDelete, // () => void - onReplace, // () => void -}) { +type DeleteReasonModalProps = { + targetRow: DocumentRow; + open: boolean; + onCancel: () => void; + /** файл удалён — обновить список */ + onDelete: () => void; + onReplace: () => void; + /** замена = загрузка нового файла: только в версии в работе, как кнопка «Загрузить документы» */ + canReplace: boolean; +}; + +/** Удаление или замена — файлом целиком (`DELETE /files/{file_id}`): уходят все документы файла. */ +export default function DeleteReasonModal({ targetRow, open, onCancel, onDelete, onReplace, canReplace }: DeleteReasonModalProps) { const [isDeleting, setIsDeleting] = useState(false); - const handleDelete = useCallback(async () => { + const handleDelete = async () => { setIsDeleting(true); try { - await removeFile(targetRow.key); + await removeFile(targetRow.fileId); message.success('Файл удалён'); - } catch (e) { - message.error(e.message); + onDelete(); + } catch (e: unknown) { + // 409 — например, в файле подтверждённый документ; сервер объясняет причину + message.error(e instanceof ApiError ? e.message : 'Не удалось удалить файл'); } finally { setIsDeleting(false); - onDelete(); } - }, [targetRow.key, onDelete]); + }; return (
@@ -66,9 +75,11 @@ export default function DeleteReasonModal({ title='Замена документа' detail='Загрузите актуальный файл и укажите причину замены.' action={ - + + + } />
diff --git a/src/widgets/FilesDrawer/FilesDrawer.tsx b/src/widgets/FilesDrawer/FilesDrawer.tsx index 789da12..7d18a64 100644 --- a/src/widgets/FilesDrawer/FilesDrawer.tsx +++ b/src/widgets/FilesDrawer/FilesDrawer.tsx @@ -1,17 +1,20 @@ import { CloseOutlined, UploadOutlined } from '@ant-design/icons'; -import { Button, Drawer, message, Switch, Tooltip, Typography } from 'antd'; +import { Button, Drawer, Switch, Tooltip, Typography, message } from 'antd'; import { useEffect, useState } from 'react'; +import { ApiError } from '../../api/client'; import { fetchLegalEntity } from '../../services/api/directories/company_groups'; +import { postComment } from '../../services/api/documents'; +import { summarizeUpload, uploadFiles } from '../../services/api/files'; import { isInWork, useCurrentConsolidation } from '../../store/slices/consolidations/selectors'; import { useAppSelector } from '../../store/store'; import type { FileOut, LegalEntityBrief } from '../../types/api'; +import DeleteReasonModal from './DeleteModal'; import { FilesList } from './FilesList'; +import ReplaceDocModal from './ReplaceDocModal'; import { UploadDocumentsModal } from './UploadDocumentsModal'; import { consolidationLabel } from './consolidationLabel'; +import type { DocumentRow } from './documentRows'; import { useLegalEntityFiles } from './useLegalEntityFiles'; -import { DocumentRow } from './documentRows'; -import DeleteReasonModal from './DeleteModal'; -import ReplaceDocModal from './ReplaceDocModal'; const UPLOAD_DISABLED_HINT = 'Загрузка доступна только в версии в работе'; @@ -65,14 +68,27 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: () setTargetRow(row); setRemovalOpen(true); }; - const handleReplaceSubmit = ({ file, comment }) => { + // Замена = загрузка нового файла в ту же консолидацию: прежний ядро переводит в SUPERSEDED после разбора, + // если совпали тип, счёт и период (05-domain-rules §9.5 п. 3). Причина — комментарием к заменяемому документу. + const handleReplaceSubmit = async (file: File, reason: string) => { + if (!current_legal_entity_id || !targetRow) return; + setReplacing(true); try { - setReplacing(true); - message.success('Документ заменен'); + const result = summarizeUpload(await uploadFiles(current_legal_entity_id, [file], current?.consolidation_id ?? null)); + if (result.accepted.length === 0) { + message.error(result.rejected[0]?.reason ?? 'Файл не принят'); + return; + } + if (targetRow.documentId) { + await postComment(targetRow.documentId, `Замена документа: ${reason}`).catch(() => + message.warning('Файл загружен, но причину замены сохранить не удалось'), + ); + } + message.success('Файл загружен: прежний будет заменён после разбора, если совпадут тип, счёт и период'); setReplaceOpen(false); - reload(); - } catch (e) { - message.error(e.message); + void reload(); + } catch (e: unknown) { + message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить файл'); } finally { setReplacing(false); } @@ -158,10 +174,12 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: () onCancel={() => setRemovalOpen(false)} onDelete={onDelete} onReplace={handleReplaceClick} + canReplace={canUpload} /> setReplaceOpen(false)} onSubmit={handleReplaceSubmit} diff --git a/src/widgets/FilesDrawer/FilesList.tsx b/src/widgets/FilesDrawer/FilesList.tsx index 26b4774..7c74d7c 100644 --- a/src/widgets/FilesDrawer/FilesList.tsx +++ b/src/widgets/FilesDrawer/FilesList.tsx @@ -1,5 +1,5 @@ import { CheckOutlined, CloseOutlined, DeleteOutlined, FileExcelOutlined, FileImageOutlined, FilePdfOutlined } from '@ant-design/icons'; -import { Button, message, Spin, Table, Tooltip, Typography } from 'antd'; +import { Button, Spin, Table, Tooltip, Typography, message } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -140,6 +140,7 @@ export function FilesList({ render: (_, r) => ( diff --git a/src/widgets/FilesDrawer/documentRows.test.ts b/src/widgets/FilesDrawer/documentRows.test.ts index 88683a7..d99e3fc 100644 --- a/src/widgets/FilesDrawer/documentRows.test.ts +++ b/src/widgets/FilesDrawer/documentRows.test.ts @@ -145,6 +145,13 @@ describe('buildDocumentRows: предупреждения', () => { expect(rows[0].issueCounts).toEqual({ error: 0, info: 0 }); }); + it('fileId — id файла и у строки документа, и у строки файла без документов (удаление идёт по файлу)', () => { + const withDoc = buildDocumentRows([file({ id: 'f7', documents: [doc({ id: 'd7' })] })]); + expect(withDoc[0]).toMatchObject({ key: 'd7', documentId: 'd7', fileId: 'f7' }); + const failed = buildDocumentRows([file({ id: 'f8', status: 'ERROR' })]); + expect(failed[0]).toMatchObject({ key: 'f8', documentId: null, fileId: 'f8' }); + }); + it('job null — пустые списки; старый формат строк не ломает', () => { expect(buildDocumentRows([file({ documents: [doc()] })])[0]).toMatchObject({ fileWarnings: [], mappingWarnings: [] }); const legacy = buildDocumentRows([file({ job: job({ warnings: ['старое'] as never }), documents: [doc()] })]); diff --git a/src/widgets/FilesDrawer/documentRows.ts b/src/widgets/FilesDrawer/documentRows.ts index 369e959..2a62265 100644 --- a/src/widgets/FilesDrawer/documentRows.ts +++ b/src/widgets/FilesDrawer/documentRows.ts @@ -10,6 +10,8 @@ export interface DocumentRow { key: string; /** id документа; null — строка файла без документов */ documentId: string | null; + /** id файла: удаление и замена — файлом целиком */ + fileId: string; name: string; fileType: FileType; subtitle: string; @@ -20,8 +22,6 @@ export interface DocumentRow { statusLabel: string; errorReason: string | null; linkable: boolean; - consolidationId: string | null; - legalEntityId: string | null; /** предупреждения разбора файла (общие для всех документов файла) */ fileWarnings: FileWarningOut[]; mappingWarnings: string[]; @@ -66,6 +66,7 @@ export function buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => s { key: f.id, documentId: null, + fileId: f.id, name, fileType: f.file_type, subtitle: join([f.file_type, date, extra]), @@ -74,8 +75,6 @@ export function buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => s statusLabel: failed ? 'Ошибка' : 'Обработка', errorReason: errorReason(f), linkable: false, - consolidationId: f.consolidation_id, - legalEntityId: f.legal_entity_id, fileWarnings, mappingWarnings, issueCounts: { error: 0, info: 0 }, @@ -90,10 +89,8 @@ export function buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => s const status = documentRowStatus(d.status); return { key: d.id, - consolidationId: f.consolidation_id, documentId: d.id, - file_id: f.id, - legalEntityId: f.legal_entity_id, + fileId: f.id, name, fileType: f.file_type, subtitle: join([typeText, f.file_type, date, extra]),