fix: версия в URL, ошибки списка, подпись квартала, Назад по прямой ссылке, код ошибки не в заголовке (I-1, M-3, M-4, M-6, T7.M7, T7.M4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
This commit is contained in:
Andrey Epifancev 2026-10-08 01:54:37 +04:00
parent 384301ecbd
commit 0f734e3bf8
8 changed files with 49 additions and 15 deletions

View File

@ -16,7 +16,10 @@ export default function PageTitle({ location, header, description, backable = fa
<div> <div>
<div style={{ fontSize: 12, color: '#667085' }}> <div style={{ fontSize: 12, color: '#667085' }}>
{backable && ( {backable && (
<Button type='default' onClick={() => navigate(-1)} style={{ marginRight: '5px' }}> <Button
type='default'
onClick={() => (window.history.state?.idx === 0 ? navigate('/groups') : navigate(-1))}
style={{ marginRight: '5px' }}>
<ArrowLeftOutlined /> <ArrowLeftOutlined />
Назад Назад
</Button> </Button>

View File

@ -8,6 +8,7 @@ import { getVersionCard } from '../services/api/consolidations';
import { exportOsv } from '../services/api/osv'; import { exportOsv } from '../services/api/osv';
import type { VersionCard } from '../types/consolidations/version_members'; import type { VersionCard } from '../types/consolidations/version_members';
import { exportFilename, saveBlob } from '../utils/download'; import { exportFilename, saveBlob } from '../utils/download';
import { quarterLabel } from '../utils/labels';
import { statusMapping } from '../widgets/ConsoleTable/consts'; import { statusMapping } from '../widgets/ConsoleTable/consts';
import { OsvFilters } from '../widgets/ConsolidatedOsv/OsvFilters'; import { OsvFilters } from '../widgets/ConsolidatedOsv/OsvFilters';
import { OsvGaps } from '../widgets/ConsolidatedOsv/OsvGaps'; import { OsvGaps } from '../widgets/ConsolidatedOsv/OsvGaps';
@ -52,7 +53,7 @@ function OsvContent({ versionId }: { versionId: string }) {
<PageTitle <PageTitle
backable backable
location={`Отчётность › ${card?.company_group.name ?? ''} › Сводная ОСВ`} location={`Отчётность › ${card?.company_group.name ?? ''} › Сводная ОСВ`}
header={card ? `Сводная ОСВ ${card.year} ${card.quarter} · версия ${card.version_no}` : 'Сводная ОСВ'} header={card ? `Сводная ОСВ ${card.year}, ${quarterLabel(card.quarter)} · версия ${card.version_no}` : 'Сводная ОСВ'}
description={card ? (statusMapping[card.status] ?? card.status) : ''} description={card ? (statusMapping[card.status] ?? card.status) : ''}
/> />
<main <main

View File

@ -11,6 +11,13 @@ import { ConsoleLegalEntitiesTable } from '../widgets/ConsoleLegalEntities/Conso
import { ConsoleTableWidget } from '../widgets/ConsoleTable/ConsoleTable'; import { ConsoleTableWidget } from '../widgets/ConsoleTable/ConsoleTable';
import { DocumentsDrawer } from '../widgets/FilesDrawer/FilesDrawer'; import { DocumentsDrawer } from '../widgets/FilesDrawer/FilesDrawer';
// unwrap() бросает rejectValue (строку), fetch группы — ApiError
function errorText(e: unknown): string {
if (e instanceof ApiError) return e.message;
if (typeof e === 'string') return e;
return 'Не удалось загрузить группу компаний';
}
export function ConsolidationPage() { export function ConsolidationPage() {
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [reloadKey, setReloadKey] = useState(0); const [reloadKey, setReloadKey] = useState(0);
@ -31,17 +38,25 @@ export function ConsolidationPage() {
setComGroupName(data.name); setComGroupName(data.name);
setLocationPath(`Отчетность>${data.name}>Консолидации`); setLocationPath(`Отчетность>${data.name}>Консолидации`);
setDesc(`${data.legal_entities_count} организаций`); setDesc(`${data.legal_entities_count} организаций`);
await dispatch(getConsolidationsListThunk(id)); await dispatch(getConsolidationsListThunk(id)).unwrap();
if (versionParam) dispatch(setSelectedVersion(versionParam));
} catch (e: unknown) { } catch (e: unknown) {
message.error(e instanceof ApiError ? e.message : 'Не удалось загрузить группу компаний'); message.error(errorText(e));
} }
} }
getAllInfo(group_id); getAllInfo(group_id);
}, [dispatch, group_id]);
// выбор версии следует за ?version= и не перезагружает список
useEffect(() => {
dispatch(setSelectedVersion(versionParam));
}, [dispatch, versionParam]);
// уход со страницы сбрасывает выбор (при смене группы ?version= пропадает из URL — выбор сбросит эффект выше)
useEffect(() => {
return () => { return () => {
dispatch(setSelectedVersion(null)); dispatch(setSelectedVersion(null));
}; };
}, [dispatch, group_id, versionParam]); }, [dispatch]);
return ( return (
<div <div
style={{ style={{

View File

@ -1,5 +1,13 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { counterpartyTypeLabel, documentStatusLabel, documentTypeLabel, fileStatusLabel, matchKindLabel, periodLabel } from './labels'; import {
counterpartyTypeLabel,
documentStatusLabel,
documentTypeLabel,
fileStatusLabel,
matchKindLabel,
periodLabel,
quarterLabel,
} from './labels';
describe('labels', () => { describe('labels', () => {
it('тип документа', () => { it('тип документа', () => {
@ -10,6 +18,10 @@ describe('labels', () => {
expect(documentTypeLabel('DISCLOSURE', null)).toBe('Расшифровка'); expect(documentTypeLabel('DISCLOSURE', null)).toBe('Расшифровка');
expect(documentTypeLabel('OTHER', null)).toBe('Прочее'); expect(documentTypeLabel('OTHER', null)).toBe('Прочее');
}); });
it('квартал', () => {
expect(quarterLabel('Q1')).toBe('1 квартал');
expect(quarterLabel('Q3')).toBe('3 квартал');
});
it('период', () => { it('период', () => {
expect(periodLabel(2024, 'Q1', false)).toBe('2024 К1'); expect(periodLabel(2024, 'Q1', false)).toBe('2024 К1');
expect(periodLabel(2024, 'Q3', true)).toBe('2024 · 9 мес.'); expect(periodLabel(2024, 'Q3', true)).toBe('2024 · 9 мес.');

View File

@ -14,6 +14,8 @@ export function documentTypeLabel(type: DocumentType, account: string | null): s
return type === 'OSV' && account ? `ОСВ ${account}` : TYPE_LABELS[type]; return type === 'OSV' && account ? `ОСВ ${account}` : TYPE_LABELS[type];
} }
export const quarterLabel = (q: Quarter): string => `${q.slice(1)} квартал`;
export function periodLabel(year: number | null, quarter: Quarter | null, isCumulative: boolean | null): string { export function periodLabel(year: number | null, quarter: Quarter | null, isCumulative: boolean | null): string {
if (year === null || quarter === null) { if (year === null || quarter === null) {
return 'период не определён'; return 'период не определён';

View File

@ -2,11 +2,10 @@ import { ArrowRightOutlined, DownOutlined, MoreOutlined, RightOutlined } from '@
import { Button, Dropdown, Space, message } from 'antd'; import { Button, Dropdown, Space, message } from 'antd';
import type { ColumnsType, TableProps } from 'antd/es/table'; import type { ColumnsType, TableProps } from 'antd/es/table';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { ColumnSettings, DataTable, type TableConfig, isGroup, useFilteredRows, useVisibleColumns } from '../../components/GenericTable'; import { ColumnSettings, DataTable, type TableConfig, isGroup, useFilteredRows, useVisibleColumns } from '../../components/GenericTable';
import { useTableFilters } from '../../components/GenericTable/useTableFilters'; import { useTableFilters } from '../../components/GenericTable/useTableFilters';
import { setSelectedVersion } from '../../store/slices/consolidations/consolidationsSlice'; import { useAppSelector } from '../../store/store';
import { useAppDispatch, useAppSelector } from '../../store/store';
import type { ConsRow } from '../../types/consolidations/consolidations'; import type { ConsRow } from '../../types/consolidations/consolidations';
import { consConfigBase } from './config'; import { consConfigBase } from './config';
import { flattenCons } from './flatten'; import { flattenCons } from './flatten';
@ -45,7 +44,7 @@ const RowActions = memo(function RowActions({
export function ConsoleTableWidget() { export function ConsoleTableWidget() {
const navigate = useNavigate(); const navigate = useNavigate();
const { group_id } = useParams(); const { group_id } = useParams();
const dispatch = useAppDispatch(); const [, setSearchParams] = useSearchParams();
const consolidations = useAppSelector((s) => s.consolidations.consolidations); const consolidations = useAppSelector((s) => s.consolidations.consolidations);
const loading = useAppSelector((s) => s.consolidations.loading); const loading = useAppSelector((s) => s.consolidations.loading);
@ -124,7 +123,7 @@ export function ConsoleTableWidget() {
render: (_: unknown, row: ConsRow) => ( render: (_: unknown, row: ConsRow) => (
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<Button type='default' onClick={() => navigate(`/groups/${group_id}/versions/${row.id}/osv`)}> <Button type='default' onClick={() => navigate(`/groups/${group_id}/versions/${row.id}/osv`)}>
К отчетности <ArrowRightOutlined /> К отчётности <ArrowRightOutlined />
</Button> </Button>
<RowActions row={row} onAction={handleDropdownAction} /> <RowActions row={row} onAction={handleDropdownAction} />
</div> </div>
@ -135,7 +134,8 @@ export function ConsoleTableWidget() {
); );
const onRow: TableProps<ConsRow>['onRow'] = (row) => ({ const onRow: TableProps<ConsRow>['onRow'] = (row) => ({
onClick: () => dispatch(setSelectedVersion(row.id)), // выбранная версия живёт в URL (?version=): переживает уход на «К отчётности» и «Назад»
onClick: () => setSearchParams({ version: row.id }, { replace: true }),
}); });
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>

View File

@ -88,7 +88,7 @@ export function OsvDocument({ document, onChanged }: { document: DocumentOut; on
showIcon showIcon
closable closable
onClose={() => setProblem(null)} onClose={() => setProblem(null)}
message={problem.code ? `${problem.message} (${problem.code})` : problem.message} message={<span title={problem.code}>{problem.message}</span>}
description={ description={
problem.failures.length > 0 ? ( problem.failures.length > 0 ? (
<ul style={{ margin: 0, paddingLeft: 18 }}> <ul style={{ margin: 0, paddingLeft: 18 }}>

View File

@ -14,6 +14,7 @@ import { type ReactNode, memo, useState } from 'react';
import { NavLink, useNavigate } from 'react-router-dom'; import { NavLink, useNavigate } from 'react-router-dom';
import { USERS_ADMIN } from '../../auth/permissions'; import { USERS_ADMIN } from '../../auth/permissions';
import { createConsolidation } from '../../services/api/consolidations'; import { createConsolidation } from '../../services/api/consolidations';
import { quarterLabel } from '../../utils/labels';
import { NewConsolidationModal } from '../NewConsoleModal/NewConsoleModal'; import { NewConsolidationModal } from '../NewConsoleModal/NewConsoleModal';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -216,7 +217,7 @@ export function Sidebar() {
onOk={async (values) => { onOk={async (values) => {
const card = await createConsolidation(values); const card = await createConsolidation(values);
setModalOpen(false); setModalOpen(false);
message.success(`Создана консолидация ${values.year} ${values.quarter}`); message.success(`Создана консолидация ${values.year}, ${quarterLabel(values.quarter)}`);
navigate(`/groups/${values.company_group_id}?version=${card.id}`); navigate(`/groups/${values.company_group_id}?version=${card.id}`);
}} }}
/> />