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={