Veronika Tsygankova ca19d654da [AURORA-1916] - Панель «Первичные документы»: файлы сверх первой сотни не отображаются:
- В панели отображается общее число файлов и догружаются следующие страницы (при прокрутке).
- Опрос статусов обработки файлов (раз в 5 секунд) не сбрасывает уже подгруженные страницы.
- Тот же список используется на экране проверки распознавания
2026-11-10 17:43:01 +03:00

88 lines
3.8 KiB
TypeScript
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.

import { ApiError, api, apiFetch, apiFetchBlob } from '../../api/client';
import type { FileOut, Page, UploadResult } from '../../types/api';
export function isUploadResult(body: unknown): body is UploadResult {
return (
typeof body === 'object' &&
body !== null &&
Array.isArray((body as UploadResult).accepted) &&
Array.isArray((body as UploadResult).rejected)
);
}
/** Маршрут файлов ЮЛ: с консолидацией — файлы в контексте консолидации, без неё — файлы ЮЛ (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`;
}
/** Загрузка файлов ЮЛ одним запросом (в консолидацию или без контекста); 422 «ни один не принят» возвращается как результат, прочие ошибки — throw. */
export async function uploadFiles(legalEntityId: string, files: File[], consolidationId: string | null): Promise<UploadResult> {
const form = new FormData();
for (const file of files) {
form.append('files', file);
}
try {
return await apiFetch<UploadResult>(filesPath(legalEntityId, consolidationId), { method: 'POST', body: form });
} catch (e: unknown) {
if (e instanceof ApiError && e.status === 422 && isUploadResult(e.body)) {
return e.body;
}
throw e;
}
}
export function summarizeUpload(result: UploadResult): { accepted: string[]; rejected: { name: string; reason: string }[] } {
return {
accepted: result.accepted.map((a) => a.name),
rejected: result.rejected.map((r) => ({ name: r.name, reason: r.message })),
};
}
/** Размер страницы: столько файлов панель загружает сразу и при каждой прокрутке до конца списка. Не больше 200 — `MAX_PAGE_LIMIT` бэка. */
export const FILES_PAGE_LIMIT = 200;
/** Страница файлов ЮЛ одним запросом. Бэк сортирует по дате загрузки и `id` — порядок стабилен. */
export function listLegalEntityFiles(le: string, cons: string | null, offset = 0, limit = FILES_PAGE_LIMIT) {
return apiFetch<Page<FileOut>>(`${filesPath(le, cons)}?limit=${limit}&offset=${offset}`);
}
/**
* Файлы ЮЛ начиная с номера `from` запросами по `FILES_PAGE_LIMIT`, пока не наберём `count`
* или файлы не кончатся. Пустая страница раньше `total` — файлы удалили между запросами, дальше не идём.
*/
export async function listLegalEntityFilesFrom(
le: string,
cons: string | null,
from: number,
count: number,
): Promise<{ items: FileOut[]; total: number }> {
const items: FileOut[] = [];
let total = 0;
let lastPageEmpty = false;
do {
const page = await listLegalEntityFiles(le, cons, from + items.length);
items.push(...page.items);
total = page.total;
lastPageEmpty = page.items.length === 0;
} while (items.length < count && from + items.length < total && !lastPageEmpty);
return { items, total };
}
export function hasPendingFiles(files: FileOut[]): boolean {
return files.some((f) => f.status === 'UPLOADED' || f.status === 'PROCESSING');
}
export function fetchFileContent(fileId: string): Promise<{ blob: Blob; filename: string | null }> {
return apiFetchBlob(`/files/${fileId}/content?disposition=inline`);
}
export function getFile(id: string): Promise<FileOut> {
return apiFetch<FileOut>(`/files/${id}`);
}
export function removeFile(fileId: string) {
return api.del(`/files/${fileId}`);
}