# Файлы в контексте консолидации — план для arm-web > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Панель «Первичные документы» [39] в версии показывает рабочий стол консолидации, загружает файлы в консолидацию, умеет переключаться на «Все документы организации» с подписью консолидации у файла и не даёт загружать в версию не «в работе». **Architecture:** Чистые функции (селектор консолидации по версии, подпись консолидации) с vitest; сервис файлов переключает маршруты по наличию `consolidationId`; хук списка принимает `consolidationId`; панель держит `showAll`. Компоненты не тестируются автоматически — ручной прогон на стенде в Playwright со сверкой с макетом [39]/[40]. **Tech Stack:** React 18 + TypeScript strict, antd 6, Redux Toolkit, vitest, biome, rsbuild (`make check`). **Spec:** `openspec/changes/console-files/` (`proposal.md`, `design.md` D-1…D-6, `specs/primary-files-ui/spec.md`); контракт ядра — `../docs/dev/frontend-api-guide.md` (запись 09.10, docs #90) и `arm-core/openspec/changes/console-files/`. ## Global Constraints - Worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web`, ветка `feat/26-console-files` от `test`; PR — в `test`. Git — `git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web …`. - Все запросы — через `src/api/client.ts` (`apiFetch`); поля типов — только из схем arm-core (`FileOut.consolidation_id: string | null`). - biome: ширина 140, одинарные кавычки, табы (как в репозитории); `tsc --noEmit` без ошибок; `make check` зелёный перед PR. - Маршруты: `GET/POST /consolidations/{cid}/legal-entities/{le}/files`; без консолидации — `/legal-entities/{le}/files`; query `consolidation_version_id` не отправлять. - Статусы «в работе»: `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`. - Подписи: «Все документы организации», «вне консолидации», «другая консолидация», «<год> К» (`quarterShortLabel`), подсказка «Загрузка доступна только в версии в работе». - Пользователь слаб во фронте: в PR объяснить решения простым языком; код-ревью TS/React — сам исполнитель. - Коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 `. ## Review Focus 1. Переход по прямой ссылке `?version=` до загрузки списка консолидаций: селектор вернёт `null`, панель не должна падать и должна работать в режиме «все» без переключателя. Тест `findCurrentConsolidation` на пустом списке в Task 1. 2. Файл текущей консоли в режиме «все» не должен получать подпись, файл другой консоли — должен. Тест `consolidationLabel` в Task 1. 3. Переключение режима при идущем опросе: старый ответ не должен затирать новый список. Хук уже защищён `requestRef`; `consolidationId` добавляется в зависимости эффекта сброса — проверяется ручным сценарием Task 4 (переключить во время «Обработка»). 4. Версия `CANCELLED`: кнопка загрузки недоступна, стол при этом показывается. Ручной сценарий Task 4. 5. `uploadFiles` с консолидацией при 422 `UploadResult` по-прежнему возвращает результат, а не бросает. Тест в Task 2. --- ### Task 1: Селектор консолидации и подпись консолидации (чистые функции) **Files:** - Create: `src/store/slices/consolidations/selectors.ts`, `src/store/slices/consolidations/selectors.test.ts`, `src/widgets/FilesDrawer/consolidationLabel.ts`, `src/widgets/FilesDrawer/consolidationLabel.test.ts` - Modify: `src/types/consolidations/consolidations.ts` (экспорт `IConsolidationVersion`), `src/types/api.ts` (`FileOut.consolidation_id`) **Interfaces:** - Produces: `findCurrentConsolidation(consolidations: IConsolidation[], versionId: string | null): CurrentConsolidation | null` с `CurrentConsolidation = { consolidation_id: string; year: number; quarter: QuarterEnum; version_status: ConsStatus }`; `IN_WORK_STATUSES: ReadonlySet`; `isInWork(status: ConsStatus): boolean`; `useCurrentConsolidation(): CurrentConsolidation | null`; `consolidationLabel(fileConsolidationId: string | null, consolidations: IConsolidation[], currentConsolidationId: string | null): string | null`. - [ ] **Step 1: Тесты (красные)** `src/store/slices/consolidations/selectors.test.ts`: ```ts import { describe, expect, it } from 'vitest'; import type { IConsolidation } from '../../../types/consolidations/consolidations'; import { findCurrentConsolidation, isInWork } from './selectors'; const cons: IConsolidation[] = [ { consolidation_id: 'c-2024q3', year: 2024, quarter: 'Q3', reporting_date: '2024-10-01', versions: [ { id: 'v1', version_no: 1, status: 'VERIFIED', scenario: 'CREATE', approach: 'BASIC', lock: null, created_at: null, members_count: 2 }, { id: 'v2', version_no: 2, status: 'INITIATED', scenario: 'CREATE', approach: 'BASIC', lock: null, created_at: null, members_count: 2 }, ], }, { consolidation_id: 'c-2024q4', year: 2024, quarter: 'Q4', reporting_date: '2025-01-01', versions: [{ id: 'v3', version_no: 1, status: 'CANCELLED', scenario: 'CREATE', approach: 'BASIC', lock: null, created_at: null, members_count: 2 }], }, ]; describe('findCurrentConsolidation', () => { it('находит консолидацию и статус версии по id версии', () => { expect(findCurrentConsolidation(cons, 'v2')).toEqual({ consolidation_id: 'c-2024q3', year: 2024, quarter: 'Q3', version_status: 'INITIATED' }); expect(findCurrentConsolidation(cons, 'v3')).toEqual({ consolidation_id: 'c-2024q4', year: 2024, quarter: 'Q4', version_status: 'CANCELLED' }); }); it('null без версии, при неизвестной версии и при пустом списке (прямая ссылка до загрузки)', () => { expect(findCurrentConsolidation(cons, null)).toBeNull(); expect(findCurrentConsolidation(cons, 'nope')).toBeNull(); expect(findCurrentConsolidation([], 'v2')).toBeNull(); }); }); describe('isInWork', () => { it('в работе — три статуса', () => { expect(isInWork('INITIATED')).toBe(true); expect(isInWork('DOCUMENTS_PROCESSING')).toBe(true); expect(isInWork('DATA_CORRECTION')).toBe(true); expect(isInWork('VERIFIED')).toBe(false); expect(isInWork('CANCELLED')).toBe(false); }); }); ``` `src/widgets/FilesDrawer/consolidationLabel.test.ts`: ```ts import { describe, expect, it } from 'vitest'; import type { IConsolidation } from '../../types/consolidations/consolidations'; import { consolidationLabel } from './consolidationLabel'; const cons: IConsolidation[] = [ { consolidation_id: 'c-2024q3', year: 2024, quarter: 'Q3', reporting_date: '2024-10-01', versions: [] }, { consolidation_id: 'c-2025q1', year: 2025, quarter: 'Q1', reporting_date: '2025-04-01', versions: [] }, ]; describe('consolidationLabel', () => { it('файл текущей консоли — без подписи', () => { expect(consolidationLabel('c-2024q3', cons, 'c-2024q3')).toBeNull(); }); it('другая консоль из списка — год и квартал', () => { expect(consolidationLabel('c-2025q1', cons, 'c-2024q3')).toBe('2025 К1'); }); it('без контекста и неизвестный id', () => { expect(consolidationLabel(null, cons, 'c-2024q3')).toBe('вне консолидации'); expect(consolidationLabel('ghost', cons, 'c-2024q3')).toBe('другая консолидация'); }); it('без текущей консоли подпись есть у всех, кроме null-текущей', () => { expect(consolidationLabel('c-2024q3', cons, null)).toBe('2024 К3'); expect(consolidationLabel(null, cons, null)).toBe('вне консолидации'); }); }); ``` Run: `npx vitest run src/store/slices/consolidations/selectors.test.ts src/widgets/FilesDrawer/consolidationLabel.test.ts 2>&1 | tail -8` Expected: FAIL — модули не найдены. - [ ] **Step 2: Типы** `src/types/consolidations/consolidations.ts`: `interface IConsolidationVersion` → `export interface IConsolidationVersion`. `src/types/api.ts`, `FileOut`: после `uploaded_at` добавить `/** консолидация, в контексте которой загружен файл (v5.18); null — без контекста */ consolidation_id: string | null;`. Во всех фабриках `FileOut` в тестах (`src/services/api/files.test.ts`, `src/widgets/FilesDrawer/documentRows.test.ts`) добавить `consolidation_id: null`. - [ ] **Step 3: Реализация** `src/store/slices/consolidations/selectors.ts`: ```ts import type { ConsStatus, QuarterEnum } from '../../../types/consolidations/common_types'; import type { IConsolidation } from '../../../types/consolidations/consolidations'; import { useAppSelector } from '../../store'; /** Консолидация открытой версии и статус этой версии (design D-1): выводится из списка ГК, в сторе не хранится. */ export interface CurrentConsolidation { consolidation_id: string; year: number; quarter: QuarterEnum; version_status: ConsStatus; } /** Статусы «в работе» (§9.1): только в них разрешена загрузка файлов в консолидацию. */ export const IN_WORK_STATUSES: ReadonlySet = new Set(['INITIATED', 'DOCUMENTS_PROCESSING', 'DATA_CORRECTION']); export const isInWork = (status: ConsStatus): boolean => IN_WORK_STATUSES.has(status); export function findCurrentConsolidation(consolidations: IConsolidation[], versionId: string | null): CurrentConsolidation | null { if (!versionId) return null; for (const c of consolidations) { const version = c.versions.find((v) => v.id === versionId); if (version) return { consolidation_id: c.consolidation_id, year: c.year, quarter: c.quarter, version_status: version.status }; } return null; } export function useCurrentConsolidation(): CurrentConsolidation | null { const consolidations = useAppSelector((s) => s.consolidations.consolidations); const versionId = useAppSelector((s) => s.consolidations.current_version_id); return findCurrentConsolidation(consolidations, versionId); } ``` `src/widgets/FilesDrawer/consolidationLabel.ts`: ```ts import type { IConsolidation } from '../../types/consolidations/consolidations'; import { quarterShortLabel } from '../../utils/labels'; /** Подпись консолидации у файла в режиме «Все документы организации» (design D-4): null — файл текущей консоли. */ export function consolidationLabel( fileConsolidationId: string | null, consolidations: IConsolidation[], currentConsolidationId: string | null, ): string | null { if (fileConsolidationId === null) return 'вне консолидации'; if (fileConsolidationId === currentConsolidationId) return null; const found = consolidations.find((c) => c.consolidation_id === fileConsolidationId); return found ? `${found.year} ${quarterShortLabel(found.quarter)}` : 'другая консолидация'; } ``` Если `quarterShortLabel` принимает тип `Quarter` из `types/api`, а не `QuarterEnum`, — оба это `'Q1' | … | 'Q4'`, передавать как есть; при ошибке типов привести `found.quarter as Quarter` с импортом типа. Run: `npx vitest run src/store/slices/consolidations/selectors.test.ts src/widgets/FilesDrawer/consolidationLabel.test.ts 2>&1 | tail -6 && npx tsc --noEmit && npx biome check src 2>&1 | tail -3` Expected: PASS 7 тестов; tsc и biome чистые. - [ ] **Step 4: Коммит** ```bash git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web add src/store/slices/consolidations/selectors.ts src/store/slices/consolidations/selectors.test.ts src/widgets/FilesDrawer/consolidationLabel.ts src/widgets/FilesDrawer/consolidationLabel.test.ts src/types src/services/api/files.test.ts src/widgets/FilesDrawer/documentRows.test.ts git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web commit -q -m "$(cat <<'EOF' store: консолидация открытой версии и подпись консолидации у файла (#26) Co-Authored-By: Claude Fable 5.1 EOF )" ``` --- ### Task 2: Сервис файлов — маршруты консолидации **Files:** - Modify: `src/services/api/files.ts`, `src/services/api/files.test.ts` **Interfaces:** - Produces: `uploadFiles(legalEntityId: string, files: File[], consolidationId: string | null): Promise`; `listLegalEntityFiles(legalEntityId: string, consolidationId: string | null): Promise`; `filesPath(legalEntityId: string, consolidationId: string | null): string`. - [ ] **Step 1: Тесты (красные)** В `src/services/api/files.test.ts` блок `describe('uploadFiles')` переписать: ```ts describe('filesPath', () => { it('с консолидацией — маршрут консолидации, без неё — маршрут ЮЛ', () => { expect(filesPath('le', 'c1')).toBe('/consolidations/c1/legal-entities/le/files'); expect(filesPath('le', null)).toBe('/legal-entities/le/files'); }); }); describe('uploadFiles', () => { afterEach(() => vi.restoreAllMocks()); it('422 с UploadResult возвращается как результат (и с консолидацией)', async () => { const client = await import('../../api/client'); vi.spyOn(client, 'apiFetch').mockRejectedValue(new ApiError(422, undefined, 'x', { accepted: [], rejected: result.rejected })); await expect(uploadFiles('le', [new File(['a'], 'dup.xlsx')], 'c1')).resolves.toEqual({ accepted: [], rejected: result.rejected }); }); it('422 с detail и 409 пробрасываются', async () => { const client = await import('../../api/client'); const error = new ApiError(409, 'CONSOLIDATION_NOT_EDITABLE', 'нет версии в работе', { detail: { code: 'CONSOLIDATION_NOT_EDITABLE' } }); vi.spyOn(client, 'apiFetch').mockRejectedValue(error); await expect(uploadFiles('le', [new File(['a'], 'a.xlsx')], 'c1')).rejects.toBe(error); }); it('консолидация — в пути, query нет, лишних полей формы нет', async () => { const client = await import('../../api/client'); const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result); await uploadFiles('le', [new File(['a'], 'a.xlsx'), new File(['b'], 'b.pdf')], 'c1'); const [path, init] = spy.mock.calls[0]; expect(path).toBe('/consolidations/c1/legal-entities/le/files'); const form = init?.body as FormData; expect(form.getAll('files')).toHaveLength(2); const keys: string[] = []; form.forEach((_, key) => keys.push(key)); expect(keys.every((k) => k === 'files')).toBe(true); }); it('без консолидации — маршрут ЮЛ без query', async () => { const client = await import('../../api/client'); const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue(result); await uploadFiles('le', [new File(['a'], 'a.xlsx')], null); expect(spy.mock.calls[0][0]).toBe('/legal-entities/le/files'); }); }); describe('listLegalEntityFiles', () => { afterEach(() => vi.restoreAllMocks()); it('стол консолидации и список ЮЛ', async () => { const client = await import('../../api/client'); const spy = vi.spyOn(client, 'apiFetch').mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 }); await listLegalEntityFiles('le', 'c1'); await listLegalEntityFiles('le', null); expect(spy.mock.calls[0][0]).toBe('/consolidations/c1/legal-entities/le/files?limit=100'); expect(spy.mock.calls[1][0]).toBe('/legal-entities/le/files?limit=100'); }); }); ``` Импорт дополнить `filesPath`, `listLegalEntityFiles`. Run: `npx vitest run src/services/api/files.test.ts 2>&1 | tail -8` Expected: FAIL — `filesPath` не экспортируется, пути с query. - [ ] **Step 2: Реализация** В `src/services/api/files.ts`: ```ts /** Маршрут файлов ЮЛ: с консолидацией — рабочий стол / загрузка в консолидацию, без неё — файлы ЮЛ (design D-2). */ export function filesPath(legalEntityId: string, consolidationId: string | null): string { return consolidationId ? `/consolidations/${encodeURIComponent(consolidationId)}/legal-entities/${legalEntityId}/files` : `/legal-entities/${legalEntityId}/files`; } ``` `uploadFiles(legalEntityId, files, consolidationId: string | null)`: вместо `query` — `apiFetch(filesPath(legalEntityId, consolidationId), { method: 'POST', body: form })`. `listLegalEntityFiles(legalEntityId, consolidationId: string | null)`: `apiFetch>(`${filesPath(legalEntityId, consolidationId)}?limit=100`)`. Docstring `uploadFiles` — «в консолидацию или без контекста». Run: `npx vitest run src/services/api/files.test.ts 2>&1 | tail -6 && npx tsc --noEmit 2>&1 | tail -5` Expected: тесты PASS; tsc покажет ошибки в вызовах `useLegalEntityFiles`/`UploadDocumentsModal` — это ожидаемо до Task 3 (если ошибок нет — убедиться, что сигнатура действительно изменилась). - [ ] **Step 3: Коммит** ```bash git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web add src/services/api/files.ts src/services/api/files.test.ts git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web commit -q -m "$(cat <<'EOF' api: загрузка и список файлов по маршрутам консолидации (#26) Co-Authored-By: Claude Fable 5.1 EOF )" ``` --- ### Task 3: Панель — стол, переключатель, подпись, недоступная загрузка **Files:** - Modify: `src/widgets/FilesDrawer/useLegalEntityFiles.ts`, `src/widgets/FilesDrawer/FilesDrawer.tsx`, `src/widgets/FilesDrawer/FilesList.tsx`, `src/widgets/FilesDrawer/documentRows.ts`, `src/widgets/FilesDrawer/documentRows.test.ts`, `src/widgets/FilesDrawer/UploadDocumentsModal.tsx` **Interfaces:** - Consumes: `useCurrentConsolidation`, `isInWork`, `consolidationLabel` (Task 1); `uploadFiles`, `listLegalEntityFiles` (Task 2). - Produces: `useLegalEntityFiles(legalEntityId: string | null, consolidationId: string | null, open: boolean)`; `buildDocumentRows(files: FileOut[], labelFor?: (f: FileOut) => string | null): DocumentRow[]`; `FilesList({ files, loading, labelFor? })`; `UploadDocumentsModal` принимает `consolidationId: string | null` вместо `versionId`. - [ ] **Step 1: Тест строк с подписью (красный)** В `src/widgets/FilesDrawer/documentRows.test.ts` добавить: ```ts it('подпись консолидации добавляется в subtitle последней, null — не добавляется', () => { const rows = buildDocumentRows( [file({ id: 'f1', documents: [doc()] }), file({ id: 'f2', documents: [], consolidation_id: 'c9' })], (f) => (f.id === 'f2' ? 'вне консолидации' : null), ); expect(rows[0].subtitle.endsWith('вне консолидации')).toBe(false); expect(rows[1].subtitle.endsWith(' · вне консолидации')).toBe(true); }); ``` Run: `npx vitest run src/widgets/FilesDrawer/documentRows.test.ts 2>&1 | tail -6` Expected: FAIL — второй аргумент игнорируется (tsc: лишний аргумент). - [ ] **Step 2: `documentRows.ts` и `FilesList.tsx`** `buildDocumentRows(files, labelFor?)`: `const extra = labelFor?.(f) ?? null;` в начале `flatMap`; в обеих ветках `subtitle: join([... , extra])` (для файла без документов — `join([f.file_type, date, extra])`, для документа — `join([typeText, f.file_type, date, extra])`). `FilesList({ files, loading, labelFor }: { files: FileOut[]; loading: boolean; labelFor?: (f: FileOut) => string | null })` → `buildDocumentRows(files, labelFor)`. Run: `npx vitest run src/widgets/FilesDrawer/documentRows.test.ts 2>&1 | tail -4` Expected: PASS. - [ ] **Step 3: Хук** `useLegalEntityFiles(legalEntityId, consolidationId, open)`: `listLegalEntityFiles(legalEntityId, consolidationId)` в `reload` (зависимости `[legalEntityId, consolidationId]`); эффект сброса — зависимости `[open, legalEntityId, consolidationId, reload]`, комментарий «при смене ЮЛ, режима (консолидация/все) или закрытии панели…». Docstring: «Файлы рабочего стола консолидации (`consolidationId`) или все файлы ЮЛ (`null`)». - [ ] **Step 4: Панель и модалка** `UploadDocumentsModal`: prop `versionId` → `consolidationId: string | null`; `uploadFiles(legalEntityId, files, consolidationId)`. `FilesDrawer.tsx`: ```tsx import { CloseOutlined, UploadOutlined } from '@ant-design/icons'; import { Button, Drawer, Switch, Tooltip, Typography } from 'antd'; import { useEffect, useState } from 'react'; import { fetchLegalEntity } from '../../services/api/directories/company_groups'; import { isInWork, useCurrentConsolidation } from '../../store/slices/consolidations/selectors'; import { useAppSelector } from '../../store/store'; import type { FileOut, LegalEntityBrief } from '../../types/api'; import { consolidationLabel } from './consolidationLabel'; import { FilesList } from './FilesList'; import { UploadDocumentsModal } from './UploadDocumentsModal'; import { useLegalEntityFiles } from './useLegalEntityFiles'; const UPLOAD_DISABLED_HINT = 'Загрузка доступна только в версии в работе'; export function DocumentsDrawer({ open, onClose }: { open: boolean; onClose: () => void }) { const [uploadOpen, setUploadOpen] = useState(false); const [showAll, setShowAll] = useState(false); const [entity, setEntity] = useState(null); const { current_legal_entity_id, consolidations } = useAppSelector((s) => s.consolidations); const current = useCurrentConsolidation(); // консолидация не определена (версии нет в списке ГК) — режим «все» без переключателя (design D-3) const desk = current !== null && !showAll; const consolidationId = desk ? current.consolidation_id : null; const { files, loading, reload } = useLegalEntityFiles(current_legal_entity_id, consolidationId, open); const canUpload = current !== null && isInWork(current.version_status); const labelFor = desk ? undefined : (f: FileOut) => consolidationLabel(f.consolidation_id, consolidations, current?.consolidation_id ?? null); useEffect(() => { setShowAll(false); // новая панель открывается столом консоли setEntity(null); if (!open || !current_legal_entity_id) return; let cancelled = false; fetchLegalEntity(current_legal_entity_id) .then((e) => { if (!cancelled) setEntity(e); }) .catch(() => { // без имени: шапка остаётся с подписью, панель не падает }); return () => { cancelled = true; }; }, [open, current_legal_entity_id]); const uploadButton = ( ); return (
{canUpload ? uploadButton : {uploadButton}} {current !== null && ( Все документы организации )}
{current_legal_entity_id && ( setUploadOpen(false)} onUploaded={() => void reload()} /> )}
); } ``` Шапку `Drawer` (title, extra, size, closable) оставить как сейчас. `Tooltip` на disabled-кнопке требует обёртки `` — antd не вешает события на disabled button. Run: `make check 2>&1 | tail -12` Expected: biome, tsc, vitest, build — зелёные. - [ ] **Step 5: Коммит** ```bash git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web add src/widgets/FilesDrawer git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web commit -q -m "$(cat <<'EOF' drawer: рабочий стол консолидации, переключатель «Все документы организации», загрузка только в версии в работе (#26) Co-Authored-By: Claude Fable 5.1 EOF )" ``` --- ### Task 4: Ручной прогон на стенде, README, PR **Files:** - Modify: `README.md` (раздел про панель файлов, если есть упоминание `consolidation_version_id` — `grep -n consolidation_version_id README.md docs`), `CLAUDE.md` (строка про `FilesDrawer`) - [ ] **Step 1: Предусловие — стенд с ядром #96** Проверить: `curl -s -o /dev/null -w '%{http_code}' localhost:8000/api/v1/openapi.json` и в OpenAPI есть путь `/api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (`curl -s localhost:8000/api/v1/openapi.json | grep -c "legal-entities/{legal_entity_id}/files"` ≥ 2). Если стенда с ядром #96 нет — ручной прогон записать в ledger как `deferred` (O-1: стенд не обновлён), выполнить остальные шаги и указать это в PR. - [ ] **Step 2: Ручной сценарий в Playwright (сверка с макетом `../docs/mockup/screens/` для [39]/[40])** `make start` (порт 3000, прокси на :8000), затем через Playwright MCP: 1. Войти (`ka_go`), открыть ГК «Русский бисквит» → консолидация 2024 Q3 → строка ЮЛ → панель. Ожидание: заголовок «Первичные документы», список стола, переключатель «Все документы организации» выключен, кнопка загрузки активна. 2. Включить переключатель: появились файлы других периодов с подписями «2024 К4» / «вне консолидации»; у файлов текущей консоли подписи нет. Скриншот. 3. Выключить, загрузить файл из `../fixtures/osv/rb/2024-Q3/` через [40]: модалка без полей типа/периода, итог «Принято: 1», в столе строка «Обработка», через ≤15 с — документ. Во время «Обработка» переключить режим туда и обратно — список не «прыгает» на старый ответ (Review Focus 3). 4. Открыть панель из версии `CANCELLED` (создать через API `POST …/status {CANCELLED, comment}` на тестовой консоли или использовать существующую): кнопка недоступна, подсказка при наведении; стол показывается (Review Focus 4). 5. Прямая ссылка `/groups/{id}?version={v}` в новой вкладке → панель открывается без ошибок в консоли браузера. Скриншоты шагов 1, 2, 4 — в описание PR (пути в `.superpowers/sdd//`, не в git). - [ ] **Step 3: README / CLAUDE.md** `grep -rn "consolidation_version_id" README.md CLAUDE.md docs scripts 2>/dev/null` — заменить упоминания на маршруты консолидации (в `scripts/demo-prepare.sh` загрузка через `curl` должна идти на `/consolidations/$C/legal-entities/$LE/files`, где `C` — `consolidation_id` из ответа `POST /consolidations`); в `CLAUDE.md` в строке про `FilesDrawer` дописать «(стол консолидации / все файлы ЮЛ, подпись консолидации, селектор `store/slices/consolidations/selectors.ts`)». - [ ] **Step 4: Проверка, коммит, push, PR** ```bash cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web && make check 2>&1 | tail -15 git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web add README.md CLAUDE.md git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web commit -q -m "$(cat <<'EOF' docs: панель файлов — маршруты консолидации (#26) Co-Authored-By: Claude Fable 5.1 EOF )" git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/arm-web push -u origin feat/26-console-files ``` PR — через superpowers:finishing-a-development-branch (`tea pulls create --login home --repo RSHB/arm-web --base test --head feat/26-console-files …`): заголовок «Панель «Первичные документы»: рабочий стол консолидации и загрузка в консолидацию (#26)»; в описании — ссылки на issue #26, docs #90, arm-core #96, объяснение решений простым языком (селектор вместо поля стора, переключатель, подпись), «Rulings I made», `deferred`, вывод `make check`, скриншоты.