arm-web/docs/superpowers/plans/2026-10-09-console-files-web.md
Andrey Epifancev 490113cb06 plan: demo-prepare.sh — загрузка через маршрут консолидации (#26)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 23:47:18 +04:00

553 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Файлы в контексте консолидации — план для 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`.
- Подписи: «Все документы организации», «вне консолидации», «другая консолидация», «<год> К<n>» (`quarterShortLabel`),
подсказка «Загрузка доступна только в версии в работе».
- Пользователь слаб во фронте: в PR объяснить решения простым языком; код-ревью TS/React — сам исполнитель.
- Коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>`.
## 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<ConsStatus>`; `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<ConsStatus> = new Set<ConsStatus>(['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 <noreply@anthropic.com>
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<UploadResult>`;
`listLegalEntityFiles(legalEntityId: string, consolidationId: string | null): Promise<FileOut[]>`;
`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<UploadResult>(filesPath(legalEntityId, consolidationId), { method: 'POST', body: form })`.
`listLegalEntityFiles(legalEntityId, consolidationId: string | null)`: `apiFetch<Page<FileOut>>(`${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 <noreply@anthropic.com>
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<LegalEntityBrief | null>(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 = (
<Button
type='primary'
icon={<UploadOutlined />}
disabled={!current_legal_entity_id || !canUpload}
onClick={() => setUploadOpen(true)}
style={{ background: canUpload ? '#237804' : undefined }}>
Загрузить документы
</Button>
);
return (
<Drawer … (как сейчас) …>
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 16 }}>
{canUpload ? uploadButton : <Tooltip title={UPLOAD_DISABLED_HINT}><span>{uploadButton}</span></Tooltip>}
{current !== null && (
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Switch size='small' checked={showAll} onChange={setShowAll} />
<Typography.Text>Все документы организации</Typography.Text>
</span>
)}
</div>
<FilesList files={files} loading={loading} labelFor={labelFor} />
{current_legal_entity_id && (
<UploadDocumentsModal
legalEntityId={current_legal_entity_id}
consolidationId={current?.consolidation_id ?? null}
organizationName={entity?.name ?? null}
open={uploadOpen}
onClose={() => setUploadOpen(false)}
onUploaded={() => void reload()}
/>
)}
</Drawer>
);
}
```
Шапку `Drawer` (title, extra, size, closable) оставить как сейчас. `Tooltip` на disabled-кнопке требует обёртки
`<span>` — 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 <noreply@anthropic.com>
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/<plan>/`, не в 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 <noreply@anthropic.com>
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`, скриншоты.