feat: улучшить импорт CSV — дедупликация, кодировка, парсинг сумм

Пять доработок импорта транзакций из CSV:
1. Проверка дублей при повторном импорте — сверка с уже существующими
   транзакциями на счёте за диапазон дат файла (по дате+типу+сумме+
   описанию), похожие исключаются из импорта по умолчанию с опцией
   всё равно их добавить.
2. Автоопределение кодировки файла (UTF-8 с откатом на Windows-1251)
   вместо жёстко зашитого UTF-8 — раньше кириллица из банковских
   выгрузок превращалась в кракозябры. Есть ручной выбор на случай
   неверного автоопределения.
3. Исправлен баг в parseAmount: суммы с несколькими разделителями
   тысяч и без дробной части (например "1,234,567") теряли цифры и
   отбрасывались как нераспознанные — теперь распознаются верно.
4. Добавлен явный выбор разделителя дробной части (точка/запятая) на
   случай, когда автоопределение не может разрешить неоднозначность.
5. Пример значения из файла показывается прямо под каждым выбором
   колонки при сопоставлении — не нужно доходить до превью, чтобы
   понять, что в колонке.
This commit is contained in:
ZProger
2026-09-10 20:06:41 +03:00
parent 78826db33e
commit 777e3af725
5 changed files with 310 additions and 52 deletions

View File

@@ -26,6 +26,25 @@ export function fetchTransactions(filters: TransactionFilters = {}) {
return api.get<TransactionPage>(`/transactions?${params.toString()}`);
}
// Used by CSV import's duplicate check (see pages/CsvImportPage.tsx) — needs
// every transaction in the imported date range for one account, not just a
// page of them, to know what's already there. Capped at 25 pages (5000 rows
// at the API's max page_size) so a huge date range can't turn one import
// into an unbounded fetch loop; beyond that, duplicate detection just
// silently covers less of the range instead of hanging.
const MAX_DUPLICATE_CHECK_PAGES = 25;
export async function fetchAllTransactionsInRange(filters: TransactionFilters): Promise<Transaction[]> {
const pageSize = 200;
const items: Transaction[] = [];
for (let page = 1; page <= MAX_DUPLICATE_CHECK_PAGES; page++) {
const result = await fetchTransactions({ ...filters, page, page_size: pageSize });
items.push(...result.items);
if (items.length >= result.total || result.items.length < pageSize) break;
}
return items;
}
/** Full range of years to offer in the year picker, from the earliest
* transaction through the current year (see backend for the "gap year"
* rationale). */