Files
aurum/frontend/src/api/transactions.ts
ZProger 777e3af725 feat: улучшить импорт CSV — дедупликация, кодировка, парсинг сумм
Пять доработок импорта транзакций из CSV:
1. Проверка дублей при повторном импорте — сверка с уже существующими
   транзакциями на счёте за диапазон дат файла (по дате+типу+сумме+
   описанию), похожие исключаются из импорта по умолчанию с опцией
   всё равно их добавить.
2. Автоопределение кодировки файла (UTF-8 с откатом на Windows-1251)
   вместо жёстко зашитого UTF-8 — раньше кириллица из банковских
   выгрузок превращалась в кракозябры. Есть ручной выбор на случай
   неверного автоопределения.
3. Исправлен баг в parseAmount: суммы с несколькими разделителями
   тысяч и без дробной части (например "1,234,567") теряли цифры и
   отбрасывались как нераспознанные — теперь распознаются верно.
4. Добавлен явный выбор разделителя дробной части (точка/запятая) на
   случай, когда автоопределение не может разрешить неоднозначность.
5. Пример значения из файла показывается прямо под каждым выбором
   колонки при сопоставлении — не нужно доходить до превью, чтобы
   понять, что в колонке.
2026-09-10 20:06:41 +03:00

72 lines
2.6 KiB
TypeScript

import { api } from "@/api/client";
import type { Transaction, TransactionInput, TransactionPage } from "@/types";
export type TransactionSort = "date_desc" | "amount_desc" | "amount_asc";
export interface TransactionFilters {
year?: number;
month?: number;
start_date?: string;
end_date?: string;
account_id?: number;
category_id?: number;
tag_id?: number;
type?: string;
search?: string;
sort?: TransactionSort;
page?: number;
page_size?: number;
}
export function fetchTransactions(filters: TransactionFilters = {}) {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) params.set(key, String(value));
});
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). */
export function fetchTransactionYears() {
return api.get<number[]>("/transactions/years");
}
export function createTransaction(input: TransactionInput) {
return api.post<Transaction>("/transactions", input);
}
/** CSV import — see pages/CsvImportPage.tsx. All-or-nothing on the backend:
* either every row is created, or (on a validation error) none are. */
export function bulkCreateTransactions(items: TransactionInput[]) {
return api.post<{ created: number }>("/transactions/bulk", { items });
}
export function updateTransaction(id: number, input: Partial<TransactionInput>) {
return api.patch<Transaction>(`/transactions/${id}`, input);
}
export function deleteTransaction(id: number) {
return api.delete<void>(`/transactions/${id}`);
}