31 KiB
Файлы в контексте консолидации — план для 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; queryconsolidation_version_idне отправлять. - Статусы «в работе»:
INITIATED,DOCUMENTS_PROCESSING,DATA_CORRECTION. - Подписи: «Все документы организации», «вне консолидации», «другая консолидация», «<год> К» (
quarterShortLabel), подсказка «Загрузка доступна только в версии в работе». - Пользователь слаб во фронте: в PR объяснить решения простым языком; код-ревью TS/React — сам исполнитель.
- Коммит заканчивается строкой
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>.
Review Focus
- Переход по прямой ссылке
?version=до загрузки списка консолидаций: селектор вернётnull, панель не должна падать и должна работать в режиме «все» без переключателя. ТестfindCurrentConsolidationна пустом списке в Task 1. - Файл текущей консоли в режиме «все» не должен получать подпись, файл другой консоли — должен. Тест
consolidationLabelв Task 1. - Переключение режима при идущем опросе: старый ответ не должен затирать новый список. Хук уже защищён
requestRef;consolidationIdдобавляется в зависимости эффекта сброса — проверяется ручным сценарием Task 4 (переключить во время «Обработка»). - Версия
CANCELLED: кнопка загрузки недоступна, стол при этом показывается. Ручной сценарий Task 4. uploadFilesс консолидацией при 422UploadResultпо-прежнему возвращает результат, а не бросает. Тест в 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:
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:
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:
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:
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: Коммит
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') переписать:
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:
/** Маршрут файлов ЮЛ: с консолидацией — рабочий стол / загрузка в консолидацию, без неё — файлы ЮЛ (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: Коммит
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 добавить:
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:
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: Коммит
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:
- Войти (
ka_go), открыть ГК «Русский бисквит» → консолидация 2024 Q3 → строка ЮЛ → панель. Ожидание: заголовок «Первичные документы», список стола, переключатель «Все документы организации» выключен, кнопка загрузки активна. - Включить переключатель: появились файлы других периодов с подписями «2024 К4» / «вне консолидации»; у файлов текущей консоли подписи нет. Скриншот.
- Выключить, загрузить файл из
../fixtures/osv/rb/2024-Q3/через [40]: модалка без полей типа/периода, итог «Принято: 1», в столе строка «Обработка», через ≤15 с — документ. Во время «Обработка» переключить режим туда и обратно — список не «прыгает» на старый ответ (Review Focus 3). - Открыть панель из версии
CANCELLED(создать через APIPOST …/status {CANCELLED, comment}на тестовой консоли или использовать существующую): кнопка недоступна, подсказка при наведении; стол показывается (Review Focus 4). - Прямая ссылка
/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
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, скриншоты.