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

31 KiB
Raw Blame History

Файлы в контексте консолидации — план для 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 <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:

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:

  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
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, скриншоты.