Merge pull request '«Назад» из документа возвращает панель «Первичные документы» организации (#197, AURORA-1682)' (#198) from feat/197-back-to-primary-docs into main
This commit is contained in:
commit
ec783c8ec0
2
openspec/changes/back-to-primary-docs/.openspec.yaml
Normal file
2
openspec/changes/back-to-primary-docs/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-10-27
|
||||||
20
openspec/changes/back-to-primary-docs/design.md
Normal file
20
openspec/changes/back-to-primary-docs/design.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
AURORA-1682, RSHB/arm-core#197. Панель «Первичные документы» открывалась локальным состоянием `ConsolidationPage`
|
||||||
|
(`drawerOpen`), ЮЛ панели — в сторе (`current_legal_entity_id`). «Назад» из документа (`navigate(-1)`) монтирует страницу
|
||||||
|
консолидации заново, и панель закрыта.
|
||||||
|
|
||||||
|
## Decisions made without discussion
|
||||||
|
|
||||||
|
- **2026-10-27, R-1.** Открытая панель хранится в адресе (`?entity=<legal_entity_id>`), а не в сторе или sessionStorage.
|
||||||
|
Обоснование: выбранная версия уже живёт в `?version=` (ConsoleTable), история браузера сама возвращает адрес с панелью,
|
||||||
|
а ссылку с панелью можно переслать. Цена ошибки: если параметр не нужен в адресе — заменить на состояние в сторе
|
||||||
|
(правка в `ConsolidationsList.tsx` и двух функциях `utils/consolidations.ts`).
|
||||||
|
- **2026-10-27, R-2.** Открытие и закрытие панели заменяют запись истории (`replace: true`), как выбор версии. Обоснование:
|
||||||
|
иначе «Назад» браузера после закрытия панели снова открывал бы её. Цена ошибки: низкая — одна опция `setSearchParams`.
|
||||||
|
- **2026-10-27, R-3.** Режим «Все документы организации» в адрес не входит: после «Назад» панель открыта столом
|
||||||
|
консолидации. Обоснование: в задаче речь о возврате к панели организации; переключатель и без того сбрасывается при каждом
|
||||||
|
открытии панели. Цена ошибки: низкая — добавить второй параметр адреса.
|
||||||
|
- **2026-10-27, R-4.** Панель открывается, только когда ЮЛ из адреса уже в сторе (`current_legal_entity_id === entity`).
|
||||||
|
Обоснование: стор обновляется эффектом после первого рендера — без условия панель на мгновение показала бы прежнее ЮЛ и
|
||||||
|
запросила бы его файлы. Цена ошибки: нет (поведение для пользователя то же).
|
||||||
38
openspec/changes/back-to-primary-docs/proposal.md
Normal file
38
openspec/changes/back-to-primary-docs/proposal.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
AURORA-1682 (замечание тестирования 16.10.2026, п. 1; чек-лист Циликова 23.10.2026, строка 9): аналитик открывает документ
|
||||||
|
из панели «Первичные документы» организации, проверяет его и нажимает «Назад» — открывается карточка консолидации без
|
||||||
|
панели, и для следующего документа панель приходится открывать заново. Панель открывалась локальным состоянием страницы
|
||||||
|
консолидации (`drawerOpen`), а «Назад» (`navigate(-1)`) монтирует страницу заново.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
Репозиторий — только arm-core (`web/`), эндпоинты без изменений.
|
||||||
|
|
||||||
|
- Открытая панель «Первичные документы» хранится в адресе экрана консолидации параметром `?entity=<legal_entity_id>`
|
||||||
|
рядом с `?version=`; открытие и закрытие панели заменяют текущую запись истории (как выбор версии). Расчёт параметров —
|
||||||
|
`panelEntity` / `withPanelEntity` (`web/src/utils/consolidations.ts`), закреплён тестом.
|
||||||
|
- «Назад» из документа, открытого из панели, возвращает на адрес с `?entity=` — панель той же организации открыта.
|
||||||
|
Переключатель документа (AURORA-1692) и прямая ссылка ведут себя как прежде.
|
||||||
|
- Дельты: `document-review-ui` — MODIFIED «Переключатель документа» (сценарий «Назад после перебора документов»);
|
||||||
|
`primary-files-ui` — ADDED «Открытая панель в адресе экрана консолидации».
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Режим «Все документы организации» в адрес не попадает: после «Назад» панель открывается столом консолидации, как при
|
||||||
|
обычном открытии.
|
||||||
|
- Изменения бэкенда и DBML.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `document-review-ui`: MODIFIED «Переключатель документа».
|
||||||
|
- `primary-files-ui`: ADDED «Открытая панель в адресе экрана консолидации».
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
`web/src/layouts/ConsolidationsList.tsx`, `web/src/widgets/ConsoleLegalEntities/ConsoleLegalEntities.tsx`,
|
||||||
|
`web/src/utils/consolidations.ts` (+ тест). Ссылки на экран консолидации с `?entity=` открывают панель этой организации.
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Переключатель документа
|
||||||
|
В шапке экрана документа SHALL быть выпадающий список «Документ» с документами файлов того же ЮЛ в той же консолидации,
|
||||||
|
что и файл открытого документа (файл вне консолидации — все файлы ЮЛ): у пункта имя файла, вторая строка (тип · формат ·
|
||||||
|
дата) и статус; пункты, которые нельзя открыть, неактивны. Выбор пункта SHALL открывать `/documents/:document_id`
|
||||||
|
выбранного документа заменой текущей записи истории браузера, без новой записи: кнопка «Назад» экрана и браузера
|
||||||
|
возвращает на экран, с которого открыт первый документ, а не к предыдущему выбранному. Если первый документ открыт из
|
||||||
|
панели «Первичные документы» консолидации, «Назад» SHALL открывать экран консолидации с этой панелью той же организации.
|
||||||
|
|
||||||
|
#### Scenario: Назад после перебора документов
|
||||||
|
- **WHEN** аналитик открыл документ из панели «Первичные документы» организации в карточке консолидации, выбрал в списке
|
||||||
|
«Документ» подряд ещё два документа и нажал «Назад»
|
||||||
|
- **THEN** открыт экран консолидации с той же выбранной версией и открытой панелью «Первичные документы» той же
|
||||||
|
организации
|
||||||
|
|
||||||
|
#### Scenario: Документ открыт прямой ссылкой
|
||||||
|
- **WHEN** документ открыт прямой ссылкой в новой вкладке, аналитик выбрал в списке «Документ» другой документ и нажал
|
||||||
|
«Назад» экрана
|
||||||
|
- **THEN** открыт список групп компаний `/groups`
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Открытая панель в адресе экрана консолидации
|
||||||
|
Открытая панель «Первичные документы» SHALL храниться в адресе экрана консолидации `/groups/:group_id` параметром
|
||||||
|
`?entity=<legal_entity_id>` рядом с `?version=`. Клик по организации в панели «Организации» SHALL добавлять параметр,
|
||||||
|
закрытие панели — убирать его; оба перехода SHALL заменять текущую запись истории браузера, без новой записи. Адрес с
|
||||||
|
`?entity=` SHALL открывать панель этой организации. Режим «Все документы организации» в адрес не входит: панель,
|
||||||
|
открытая по адресу, показывает стол консолидации.
|
||||||
|
|
||||||
|
#### Scenario: Возврат из документа
|
||||||
|
- **WHEN** аналитик открыл панель организации, перешёл по ссылке документа и вернулся «Назад»
|
||||||
|
- **THEN** открыт экран консолидации с той же версией и панелью «Первичные документы» той же организации
|
||||||
|
|
||||||
|
#### Scenario: Закрытая панель не возвращается
|
||||||
|
- **WHEN** аналитик открыл панель организации, закрыл её и нажал «Назад» браузера
|
||||||
|
- **THEN** открыт экран, с которого пришли на экран консолидации, панель не открывается снова
|
||||||
11
openspec/changes/back-to-primary-docs/tasks.md
Normal file
11
openspec/changes/back-to-primary-docs/tasks.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
## 1. «Назад» на панель «Первичные документы» (AURORA-1682, RSHB/arm-core#197)
|
||||||
|
|
||||||
|
- [x] 1.1 `web/src/utils/consolidations.ts`: `panelEntity`, `withPanelEntity` — ЮЛ панели в `?entity=`; тест в
|
||||||
|
`consolidations.test.ts`.
|
||||||
|
- [x] 1.2 `web/src/layouts/ConsolidationsList.tsx`: панель открыта по `?entity=` (когда ЮЛ из адреса уже в сторе),
|
||||||
|
открытие и закрытие — `setSearchParams(…, { replace: true })`; `ConsoleLegalEntities` передаёт id ЮЛ строки.
|
||||||
|
- [x] 1.3 Дельты `document-review-ui` (MODIFIED «Переключатель документа») и `primary-files-ui` (ADDED «Открытая панель
|
||||||
|
в адресе экрана консолидации»).
|
||||||
|
- [x] 1.4 Проверка в браузере: панель организации → документ → переключить 2–3 документа → «Назад» — карточка
|
||||||
|
консолидации с панелью той же организации; закрыть панель → «Назад» браузера не открывает её снова; сверка с макетом
|
||||||
|
[39], [41].
|
||||||
@ -4,8 +4,9 @@ import { useParams, useSearchParams } from 'react-router-dom';
|
|||||||
import { ApiError } from '../api/client';
|
import { ApiError } from '../api/client';
|
||||||
import PageTitle from '../components/PageTitle';
|
import PageTitle from '../components/PageTitle';
|
||||||
import { fetchOneCompanyGroup } from '../services/api/directories/company_groups';
|
import { fetchOneCompanyGroup } from '../services/api/directories/company_groups';
|
||||||
import { setSelectedVersion } from '../store/slices/consolidations/consolidationsSlice';
|
import { setSelectedLegalEntity, setSelectedVersion } from '../store/slices/consolidations/consolidationsSlice';
|
||||||
import { useAppDispatch } from '../store/store';
|
import { useAppDispatch, useAppSelector } from '../store/store';
|
||||||
|
import { panelEntity, withPanelEntity } from '../utils/consolidations';
|
||||||
import { ConsoleLegalEntitiesTable } from '../widgets/ConsoleLegalEntities/ConsoleLegalEntities';
|
import { ConsoleLegalEntitiesTable } from '../widgets/ConsoleLegalEntities/ConsoleLegalEntities';
|
||||||
import { ConsoleTableWidget } from '../widgets/ConsoleTable/ConsoleTable';
|
import { ConsoleTableWidget } from '../widgets/ConsoleTable/ConsoleTable';
|
||||||
import { DocumentsDrawer } from '../widgets/FilesDrawer/FilesDrawer';
|
import { DocumentsDrawer } from '../widgets/FilesDrawer/FilesDrawer';
|
||||||
@ -17,12 +18,16 @@ function errorText(e: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ConsolidationPage() {
|
export function ConsolidationPage() {
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
||||||
const [reloadKey, setReloadKey] = useState(0);
|
const [reloadKey, setReloadKey] = useState(0);
|
||||||
const { group_id } = useParams();
|
const { group_id } = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const versionParam = searchParams.get('version');
|
const versionParam = searchParams.get('version');
|
||||||
|
// открытая панель «Первичные документы» — в адресе (?entity=): «Назад» из документа возвращает с той же панелью
|
||||||
|
const entityParam = panelEntity(searchParams);
|
||||||
|
// панель открывается, когда ЮЛ из адреса уже в сторе: иначе на первом рендере она показала бы прежнее ЮЛ
|
||||||
|
const panelOpen = useAppSelector((s) => entityParam !== null && s.consolidations.current_legal_entity_id === entityParam);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const setPanelEntity = (entityId: string | null) => setSearchParams((params) => withPanelEntity(params, entityId), { replace: true });
|
||||||
|
|
||||||
const [comGroupName, setComGroupName] = useState('');
|
const [comGroupName, setComGroupName] = useState('');
|
||||||
const [locationPath, setLocationPath] = useState('Отчётность>');
|
const [locationPath, setLocationPath] = useState('Отчётность>');
|
||||||
@ -48,6 +53,11 @@ export function ConsolidationPage() {
|
|||||||
dispatch(setSelectedVersion(versionParam));
|
dispatch(setSelectedVersion(versionParam));
|
||||||
}, [dispatch, versionParam]);
|
}, [dispatch, versionParam]);
|
||||||
|
|
||||||
|
// ЮЛ панели следует за ?entity=; закрытие панели выбор не сбрасывает (панель скрыта, ЮЛ без запроса не перечитывается)
|
||||||
|
useEffect(() => {
|
||||||
|
if (entityParam) dispatch(setSelectedLegalEntity(entityParam));
|
||||||
|
}, [dispatch, entityParam]);
|
||||||
|
|
||||||
// уход со страницы сбрасывает выбор (при смене группы ?version= пропадает из URL — выбор сбросит эффект выше)
|
// уход со страницы сбрасывает выбор (при смене группы ?version= пропадает из URL — выбор сбросит эффект выше)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -97,18 +107,13 @@ export function ConsolidationPage() {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
}}>
|
}}>
|
||||||
<ConsoleLegalEntitiesTable
|
<ConsoleLegalEntitiesTable reloadKey={reloadKey} onDrawerOpen={setPanelEntity} />
|
||||||
reloadKey={reloadKey}
|
|
||||||
onDrawerOpen={() => {
|
|
||||||
setDrawerOpen(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<DocumentsDrawer
|
<DocumentsDrawer
|
||||||
open={drawerOpen}
|
open={panelOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDrawerOpen(false);
|
setPanelEntity(null);
|
||||||
setReloadKey((k) => k + 1);
|
setReloadKey((k) => k + 1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -16,12 +16,14 @@ import {
|
|||||||
isLockedBy,
|
isLockedBy,
|
||||||
isValidReason,
|
isValidReason,
|
||||||
manualTransitions,
|
manualTransitions,
|
||||||
|
panelEntity,
|
||||||
parseInWorkOnly,
|
parseInWorkOnly,
|
||||||
rememberCounterPairsShown,
|
rememberCounterPairsShown,
|
||||||
reportTabNavigation,
|
reportTabNavigation,
|
||||||
resolveLeaveAfterFetch,
|
resolveLeaveAfterFetch,
|
||||||
shouldWarnCounterPairs,
|
shouldWarnCounterPairs,
|
||||||
versionLabel,
|
versionLabel,
|
||||||
|
withPanelEntity,
|
||||||
} from './consolidations';
|
} from './consolidations';
|
||||||
|
|
||||||
const lock = (userId: string): LockOut => ({
|
const lock = (userId: string): LockOut => ({
|
||||||
@ -143,6 +145,24 @@ describe('вкладки отчёта (AURORA-1851)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('панель «Первичные документы» в адресе (AURORA-1682)', () => {
|
||||||
|
it('открытие добавляет ?entity= к выбранной версии, исходные параметры не меняются', () => {
|
||||||
|
const params = new URLSearchParams('version=v1&in_work_only=true');
|
||||||
|
const next = withPanelEntity(params, 'le1');
|
||||||
|
expect(next.toString()).toBe('version=v1&in_work_only=true&entity=le1');
|
||||||
|
expect(params.toString()).toBe('version=v1&in_work_only=true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('закрытие убирает ?entity=, версия остаётся', () => {
|
||||||
|
expect(withPanelEntity(new URLSearchParams('version=v1&entity=le1'), null).toString()).toBe('version=v1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('организация панели читается из адреса', () => {
|
||||||
|
expect(panelEntity(new URLSearchParams('version=v1&entity=le1'))).toBe('le1');
|
||||||
|
expect(panelEntity(new URLSearchParams('version=v1'))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const pair = (holder: string, missing: string): CounterPairOut => ({
|
const pair = (holder: string, missing: string): CounterPairOut => ({
|
||||||
holder_id: holder,
|
holder_id: holder,
|
||||||
holder_name: `H${holder}`,
|
holder_name: `H${holder}`,
|
||||||
|
|||||||
@ -100,6 +100,23 @@ export const reportTabNavigation = (pathname: string, activeKey: string, newKey:
|
|||||||
options: { replace: true },
|
options: { replace: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Параметр адреса экрана консолидации с ЮЛ открытой панели «Первичные документы» (AURORA-1682). */
|
||||||
|
const PANEL_ENTITY_PARAM = 'entity';
|
||||||
|
|
||||||
|
/** ЮЛ открытой панели «Первичные документы» из адреса; панель закрыта — `null`. */
|
||||||
|
export const panelEntity = (params: URLSearchParams): string | null => params.get(PANEL_ENTITY_PARAM);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры адреса с открытой (`entityId`) или закрытой (`null`) панелью «Первичные документы»; исходные не меняются.
|
||||||
|
* Панель живёт в адресе, как выбранная версия: «Назад» из документа возвращает на консолидацию с той же панелью.
|
||||||
|
*/
|
||||||
|
export function withPanelEntity(params: URLSearchParams, entityId: string | null): URLSearchParams {
|
||||||
|
const next = new URLSearchParams(params);
|
||||||
|
if (entityId === null) next.delete(PANEL_ENTITY_PARAM);
|
||||||
|
else next.set(PANEL_ENTITY_PARAM, entityId);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
/** Статусы, в которых версия закрыта для правок: модалку о парах без встречных записей не показываем. */
|
/** Статусы, в которых версия закрыта для правок: модалку о парах без встречных записей не показываем. */
|
||||||
export const FINISHED_STATUSES: readonly ConsStatus[] = ['VERIFIED', 'CANCELLED', 'ARCHIVED', 'REJECTED'];
|
export const FINISHED_STATUSES: readonly ConsStatus[] = ['VERIFIED', 'CANCELLED', 'ARCHIVED', 'REJECTED'];
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { ColumnSettings, DataTable, useFilteredRows, useVisibleColumns } from '.
|
|||||||
import { useTableFilters } from '../../components/GenericTable/useTableFilters';
|
import { useTableFilters } from '../../components/GenericTable/useTableFilters';
|
||||||
import { downloadChecklistCsv } from '../../services/api/consolidations';
|
import { downloadChecklistCsv } from '../../services/api/consolidations';
|
||||||
import { getVersionCard } from '../../services/api/consolidations';
|
import { getVersionCard } from '../../services/api/consolidations';
|
||||||
import { setSelectedLegalEntity, setSelectedVersion } from '../../store/slices/consolidations/consolidationsSlice';
|
import { setSelectedVersion } from '../../store/slices/consolidations/consolidationsSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '../../store/store';
|
import { useAppDispatch, useAppSelector } from '../../store/store';
|
||||||
import type { IVersionCard, MemberRow } from '../../types/consolidations/version_members';
|
import type { IVersionCard, MemberRow } from '../../types/consolidations/version_members';
|
||||||
import { saveBlob } from '../../utils/download';
|
import { saveBlob } from '../../utils/download';
|
||||||
@ -15,7 +15,7 @@ import { membersConfig } from './config';
|
|||||||
import { normalizeData } from './normalize';
|
import { normalizeData } from './normalize';
|
||||||
import { checklistFilename, responsibleName } from './panelInfo';
|
import { checklistFilename, responsibleName } from './panelInfo';
|
||||||
|
|
||||||
export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawerOpen: () => void; reloadKey: number }) {
|
export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawerOpen: (entityId: string) => void; reloadKey: number }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const consolidation_id = useAppSelector((s) => s.consolidations.current_version_id);
|
const consolidation_id = useAppSelector((s) => s.consolidations.current_version_id);
|
||||||
@ -60,8 +60,7 @@ export function ConsoleLegalEntitiesTable({ onDrawerOpen, reloadKey }: { onDrawe
|
|||||||
const onRow = (row: MemberRow) => ({
|
const onRow = (row: MemberRow) => ({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
dispatch(setSelectedVersion(row.version_id));
|
dispatch(setSelectedVersion(row.version_id));
|
||||||
dispatch(setSelectedLegalEntity(row.id));
|
onDrawerOpen(row.id);
|
||||||
onDrawerOpen();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (consolidationData === null) {
|
if (consolidationData === null) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user