From c0dfe7c6c0bac796cbdd19a78aa38b797f326244 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 8 Oct 2026 03:16:20 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=E2=80=94=20=D0=BA=D1=80=D0=B5=D1=81=D1=82=D0=B8=D0=BA=20=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B0,=20=D0=BF=D0=BB=D0=B0=D1=88?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B1=D0=B5=D0=B7=20=D0=BE=D0=B1=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=D0=BA=D0=B8;=20=D0=B4=D0=B0=D1=82=D1=8B=20=D0=BF=D0=BE?= =?UTF-8?q?=20=D0=9C=D0=BE=D1=81=D0=BA=D0=B2=D0=B5;=20=D0=B7=D0=B0=D0=B3?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D0=BE=D0=BA=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20=D0=B8=20?= =?UTF-8?q?=D0=B2=D1=8B=D1=81=D0=BE=D1=82=D0=B0=20=D1=81=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BD=D0=B0=D1=80=D1=83=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9;=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20R-7?= =?UTF-8?q?=E2=80=A6R-12=20=D0=B2=20design.md=20(H-2,=20#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3 --- openspec/changes/demo-prototype/design.md | 11 +++++++++++ src/utils/dates.test.ts | 13 +++++++++++++ src/utils/dates.ts | 8 ++++++++ src/widgets/ConsoleTable/config.tsx | 3 ++- src/widgets/DocumentReview/ChecksPanel.tsx | 9 ++++++++- src/widgets/DocumentReview/StatementTable.tsx | 3 ++- src/widgets/DocumentReview/reviewView.test.ts | 13 ++++++++++++- src/widgets/DocumentReview/reviewView.ts | 13 +++++++++++-- src/widgets/FilesDrawer/FilesDrawer.tsx | 4 +++- src/widgets/FilesDrawer/FilesList.tsx | 4 +++- src/widgets/FilesDrawer/documentRows.ts | 9 ++------- 11 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 src/utils/dates.test.ts create mode 100644 src/utils/dates.ts diff --git a/openspec/changes/demo-prototype/design.md b/openspec/changes/demo-prototype/design.md index 7beb250..23e1fac 100644 --- a/openspec/changes/demo-prototype/design.md +++ b/openspec/changes/demo-prototype/design.md @@ -134,3 +134,14 @@ docs#58 O-1 на фронте обходится). В контуре SPA отд - 08.10.2026 — R-3, R-4: scoped re-review однострочного фикса T5.I1 и фикса T7.I1 (чистая функция с тестами) заменены проверкой контроллера по коду. Цена ошибки: низкая. - 08.10.2026 — R-5: все находки финального ревью ветки исправлены одной fix-волной (384301e, 0f734e3, 67bc2c8), M-5 (тосты после 401) отложена. Цена ошибки: низкая. - 08.10.2026 — R-6: `make start` задаёт `REACT_APP_ROOT_PATH=/` (иначе белый экран на dev-сервере, C-1). Цена ошибки: низкая — в контуре переменную задаёт платформа. +- 08.10.2026 — R-7: по замечаниям владельца (H-1, H-2) и сверке всех экранов с PNG макета (`docs/mockup/screens/`) экраны + [03], [30], [34], [39], [40], [41], [42] приведены к макету в той же ветке. Цена ошибки: низкая — только вёрстка. +- 08.10.2026 — R-8: суммы — запятая и разделители разрядов (`12 275,68`); в своде и ОСВ ноль показывается «—», в формах + ББ/ОФР — без копеек и «-» для пустых (как в макете). Цена ошибки: низкая — одна функция `formatAmount`. +- 08.10.2026 — R-9: даты загрузки и создания показываются по Москве (`Europe/Moscow`), а не по UTC-строке. Цена: низкая. +- 08.10.2026 — R-10: [34] «Дата верификации» — «—» (в `VersionBrief` нет поля, до маршрута согласования), «ГСК» — «—» + (П-3). Цена: низкая. +- 08.10.2026 — R-11: [30] вкладки Шахматка/КФО/… показаны неактивными (Срезы 5–6); фильтры свода — в заголовках колонок, + признак контрагента — в фильтре «Контрагенты». Цена: низкая. +- 08.10.2026 — R-12: [41] заголовок колонки формы без распознанной подписи — «На <дата периода>» или «Колонка N»; длинный + список нарушений — с прокруткой (120 px). Цена: низкая. diff --git a/src/utils/dates.test.ts b/src/utils/dates.test.ts new file mode 100644 index 0000000..e77b4d6 --- /dev/null +++ b/src/utils/dates.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; +import { formatMoscowDate } from './dates'; + +describe('formatMoscowDate', () => { + it('момент времени из API (UTC) — дата по Москве', () => { + expect(formatMoscowDate('2026-10-07T23:11:42.981145Z')).toBe('08.10.2026'); + expect(formatMoscowDate('2024-03-12T10:00:00Z')).toBe('12.03.2024'); + }); + it('пусто и мусор', () => { + expect(formatMoscowDate(null)).toBeNull(); + expect(formatMoscowDate('не дата')).toBeNull(); + }); +}); diff --git a/src/utils/dates.ts b/src/utils/dates.ts new file mode 100644 index 0000000..eb48df4 --- /dev/null +++ b/src/utils/dates.ts @@ -0,0 +1,8 @@ +const MOSCOW = new Intl.DateTimeFormat('ru-RU', { timeZone: 'Europe/Moscow', day: '2-digit', month: '2-digit', year: 'numeric' }); + +/** Момент времени из API (ISO, UTC) → «dd.mm.yyyy» по Москве (как считает даты бэкенд); пусто/мусор → null. */ +export function formatMoscowDate(iso: string | null | undefined): string | null { + if (!iso) return null; + const date = new Date(iso); + return Number.isNaN(date.getTime()) ? null : MOSCOW.format(date); +} diff --git a/src/widgets/ConsoleTable/config.tsx b/src/widgets/ConsoleTable/config.tsx index 0b2fdfb..6c8b354 100644 --- a/src/widgets/ConsoleTable/config.tsx +++ b/src/widgets/ConsoleTable/config.tsx @@ -2,10 +2,11 @@ import type { TableConfig } from '../../components/GenericTable'; import { TableTag } from '../../components/TableTag/TableTag'; import type { ConsRussianStatus, QuarterEnum } from '../../types/consolidations/common_types'; import type { ConsRow } from '../../types/consolidations/consolidations'; +import { formatMoscowDate } from '../../utils/dates'; import { quarterShortLabel } from '../../utils/labels'; import { statusColorMapping } from './consts'; -const ruDate = (v: unknown) => (typeof v === 'string' && v ? new Date(v).toLocaleDateString('ru') : '—'); +const ruDate = (v: unknown) => (typeof v === 'string' ? (formatMoscowDate(v) ?? '—') : '—'); export const consConfigBase: TableConfig = { rowKey: 'id', diff --git a/src/widgets/DocumentReview/ChecksPanel.tsx b/src/widgets/DocumentReview/ChecksPanel.tsx index 073aa12..7e0f8f3 100644 --- a/src/widgets/DocumentReview/ChecksPanel.tsx +++ b/src/widgets/DocumentReview/ChecksPanel.tsx @@ -8,7 +8,14 @@ export const failureText = (f: FailedCheckOut) => type Props = { checks: ChecksOut; editable: boolean; dirty: boolean; onAccept: (comment: string) => Promise }; -const failureList = (list: FailedCheckOut[]) => list.map((f) =>
{failureText(f)}
); +// длинный список нарушений (OCR) не должен выталкивать таблицу формы за экран +const failureList = (list: FailedCheckOut[]) => ( +
+ {list.map((f) => ( +
{failureText(f)}
+ ))} +
+); export function ChecksPanel({ checks, editable, dirty, onAccept }: Props) { const [open, setOpen] = useState(false); diff --git a/src/widgets/DocumentReview/StatementTable.tsx b/src/widgets/DocumentReview/StatementTable.tsx index 9546060..32b4bf9 100644 --- a/src/widgets/DocumentReview/StatementTable.tsx +++ b/src/widgets/DocumentReview/StatementTable.tsx @@ -2,6 +2,7 @@ import { Input, Table, Tooltip } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { ColumnOut, ValueOut } from '../../types/api'; import { formatAmount, parseAmountInput } from '../../utils/money'; +import { columnTitle } from './reviewView'; import { type GridRow, cellFlags, cellKey } from './statementGrid'; type Props = { @@ -36,7 +37,7 @@ export function StatementTable({ columns, rows, editable, draft, onDraftChange } { key: 'code', title: 'Код', dataIndex: 'code', width: 70, align: 'center' as const }, ...columns.map((col) => ({ key: col.id, - title: col.label ?? `Колонка ${col.column_index + 1}`, + title: columnTitle(col), width: 140, align: 'right' as const, onCell: (row: GridRow) => { diff --git a/src/widgets/DocumentReview/reviewView.test.ts b/src/widgets/DocumentReview/reviewView.test.ts index 8d2330d..108cae4 100644 --- a/src/widgets/DocumentReview/reviewView.test.ts +++ b/src/widgets/DocumentReview/reviewView.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { documentPageTitle, documentSubtitle, formTitle, formatReportDate, reviewActions, statusBadge } from './reviewView'; +import { columnTitle, documentPageTitle, documentSubtitle, formTitle, formatReportDate, reviewActions, statusBadge } from './reviewView'; describe('formatReportDate', () => { it('ISO → дд.мм.гггг', () => { @@ -58,3 +58,14 @@ describe('documentSubtitle', () => { expect(documentSubtitle({ original_name: null, file_type: 'XLSX', uploaded_at: null })).toBe('XLSX'); }); }); + +describe('columnTitle', () => { + const col = (label: string | null, period_date: string | null) => ({ id: 'c', column_index: 1, label, period_date, is_current: null }); + it('подпись колонки, если распознана', () => { + expect(columnTitle(col('На 31 декабря 2024 г.', null))).toBe('На 31 декабря 2024 г.'); + }); + it('пустая подпись — дата периода, затем номер', () => { + expect(columnTitle(col(' ', '2024-12-31'))).toBe('На 31.12.2024'); + expect(columnTitle(col(null, null))).toBe('Колонка 2'); + }); +}); diff --git a/src/widgets/DocumentReview/reviewView.ts b/src/widgets/DocumentReview/reviewView.ts index 39310bc..5ad4efa 100644 --- a/src/widgets/DocumentReview/reviewView.ts +++ b/src/widgets/DocumentReview/reviewView.ts @@ -1,4 +1,5 @@ -import type { DocumentStatus, DocumentType, FileOut } from '../../types/api'; +import type { ColumnOut, DocumentStatus, DocumentType, FileOut } from '../../types/api'; +import { formatMoscowDate } from '../../utils/dates'; import { documentStatusLabel } from '../../utils/labels'; /** '2025-09-30' (или ISO с временем) → '30.09.2025'; пусто → null; нераспознанное — как есть. */ @@ -33,5 +34,13 @@ export function statusBadge(status: DocumentStatus): { label: string; color: 'gr } export function documentSubtitle(file: Pick): string { - return [file.original_name, file.file_type, formatReportDate(file.uploaded_at)].filter((p) => p).join(' · '); + return [file.original_name, file.file_type, formatMoscowDate(file.uploaded_at)].filter((p) => p).join(' · '); +} + +/** Заголовок колонки формы: распознанная подпись → «На <дата периода>» → «Колонка N» (OCR часто не читает шапку). */ +export function columnTitle(col: Pick): string { + const label = col.label?.trim(); + if (label) return label; + const date = formatReportDate(col.period_date); + return date ? `На ${date}` : `Колонка ${col.column_index + 1}`; } diff --git a/src/widgets/FilesDrawer/FilesDrawer.tsx b/src/widgets/FilesDrawer/FilesDrawer.tsx index bdbda69..1ac9e2a 100644 --- a/src/widgets/FilesDrawer/FilesDrawer.tsx +++ b/src/widgets/FilesDrawer/FilesDrawer.tsx @@ -1,4 +1,4 @@ -import { UploadOutlined } from '@ant-design/icons'; +import { CloseOutlined, UploadOutlined } from '@ant-design/icons'; import { Button, Drawer, Typography } from 'antd'; import { useEffect, useState } from 'react'; import { fetchLegalEntity } from '../../services/api/directories/company_groups'; @@ -36,6 +36,8 @@ export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: () onClose={onClose} destroyOnHidden size={820} + closable={false} + extra={