- Nginx: проксирование /api на backend (единая точка входа) - История: стрелки ← → для переключения недель/месяцев/годов - История: сохранение фильтров и пагинации в URL при F5 - Импорт: миграция 003 — дефолтные правила категорий (PYATEROCHK, AUCHAN и др.) - Настройки: вкладка «Данные» с кнопкой «Очистить историю» - Backend: DELETE /api/transactions для удаления всех транзакций - ClearHistoryModal: подтверждение чекбоксами и вводом «УДАЛИТЬ»
176 lines
5.0 KiB
TypeScript
176 lines
5.0 KiB
TypeScript
import { pool } from '../db/pool';
|
|
import { escapeLike } from '../utils';
|
|
import type {
|
|
Transaction,
|
|
GetTransactionsParams,
|
|
PaginatedResponse,
|
|
UpdateTransactionRequest,
|
|
} from '@family-budget/shared';
|
|
|
|
export async function getTransactions(
|
|
params: GetTransactionsParams,
|
|
): Promise<PaginatedResponse<Transaction>> {
|
|
const page = params.page ?? 1;
|
|
const pageSize = [10, 50, 100].includes(params.pageSize ?? 50) ? (params.pageSize ?? 50) : 50;
|
|
const sortBy = params.sortBy === 'amount' ? 't.amount_signed' : 't.operation_at';
|
|
const sortOrder = params.sortOrder === 'asc' ? 'ASC' : 'DESC';
|
|
|
|
const conditions: string[] = [];
|
|
const values: unknown[] = [];
|
|
let idx = 1;
|
|
|
|
if (params.accountId != null) {
|
|
conditions.push(`t.account_id = $${idx++}`);
|
|
values.push(params.accountId);
|
|
}
|
|
if (params.from) {
|
|
conditions.push(`t.operation_at >= $${idx++}::date`);
|
|
values.push(params.from);
|
|
}
|
|
if (params.to) {
|
|
conditions.push(`t.operation_at < ($${idx++}::date + 1)`);
|
|
values.push(params.to);
|
|
}
|
|
if (params.direction) {
|
|
const dirs = params.direction.split(',').map((d) => d.trim());
|
|
conditions.push(`t.direction = ANY($${idx++}::text[])`);
|
|
values.push(dirs);
|
|
}
|
|
if (params.categoryId != null) {
|
|
conditions.push(`t.category_id = $${idx++}`);
|
|
values.push(params.categoryId);
|
|
}
|
|
if (params.search) {
|
|
conditions.push(`t.description ILIKE '%' || $${idx++} || '%'`);
|
|
values.push(escapeLike(params.search));
|
|
}
|
|
if (params.amountMin != null) {
|
|
conditions.push(`t.amount_signed >= $${idx++}`);
|
|
values.push(params.amountMin);
|
|
}
|
|
if (params.amountMax != null) {
|
|
conditions.push(`t.amount_signed <= $${idx++}`);
|
|
values.push(params.amountMax);
|
|
}
|
|
if (params.onlyUnconfirmed) {
|
|
conditions.push('t.is_category_confirmed = FALSE');
|
|
}
|
|
|
|
const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
|
|
const countResult = await pool.query(
|
|
`SELECT COUNT(*)::int AS total
|
|
FROM transactions t ${where}`,
|
|
values,
|
|
);
|
|
const totalItems: number = countResult.rows[0].total;
|
|
|
|
const offset = (page - 1) * pageSize;
|
|
const dataResult = await pool.query(
|
|
`SELECT
|
|
t.id,
|
|
t.operation_at,
|
|
t.account_id,
|
|
a.alias AS account_alias,
|
|
t.amount_signed,
|
|
t.commission,
|
|
t.description,
|
|
t.direction,
|
|
t.category_id,
|
|
c.name AS category_name,
|
|
t.is_category_confirmed,
|
|
t.comment
|
|
FROM transactions t
|
|
JOIN accounts a ON a.id = t.account_id
|
|
LEFT JOIN categories c ON c.id = t.category_id
|
|
${where}
|
|
ORDER BY ${sortBy} ${sortOrder}
|
|
LIMIT $${idx++} OFFSET $${idx++}`,
|
|
[...values, pageSize, offset],
|
|
);
|
|
|
|
const items: Transaction[] = dataResult.rows.map((r) => ({
|
|
id: Number(r.id),
|
|
operationAt: r.operation_at.toISOString(),
|
|
accountId: Number(r.account_id),
|
|
accountAlias: r.account_alias ?? null,
|
|
amountSigned: Number(r.amount_signed),
|
|
commission: Number(r.commission),
|
|
description: r.description,
|
|
direction: r.direction,
|
|
categoryId: r.category_id != null ? Number(r.category_id) : null,
|
|
categoryName: r.category_name ?? null,
|
|
isCategoryConfirmed: r.is_category_confirmed,
|
|
comment: r.comment ?? null,
|
|
}));
|
|
|
|
return {
|
|
items,
|
|
page,
|
|
pageSize,
|
|
totalItems,
|
|
totalPages: Math.ceil(totalItems / pageSize) || 1,
|
|
};
|
|
}
|
|
|
|
export async function updateTransaction(
|
|
id: number,
|
|
body: UpdateTransactionRequest,
|
|
): Promise<Transaction | null> {
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
let idx = 1;
|
|
|
|
if (body.categoryId !== undefined) {
|
|
fields.push(`category_id = $${idx++}`);
|
|
values.push(body.categoryId);
|
|
}
|
|
if (body.comment !== undefined) {
|
|
fields.push(`comment = $${idx++}`);
|
|
values.push(body.comment);
|
|
}
|
|
|
|
if (fields.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
fields.push('is_category_confirmed = TRUE');
|
|
fields.push('updated_at = NOW()');
|
|
|
|
values.push(id);
|
|
|
|
const result = await pool.query(
|
|
`UPDATE transactions SET ${fields.join(', ')} WHERE id = $${idx}
|
|
RETURNING *`,
|
|
values,
|
|
);
|
|
|
|
if (result.rows.length === 0) return null;
|
|
const r = result.rows[0];
|
|
|
|
const accResult = await pool.query('SELECT alias FROM accounts WHERE id = $1', [r.account_id]);
|
|
const catResult = r.category_id
|
|
? await pool.query('SELECT name FROM categories WHERE id = $1', [r.category_id])
|
|
: { rows: [] };
|
|
|
|
return {
|
|
id: Number(r.id),
|
|
operationAt: r.operation_at.toISOString(),
|
|
accountId: Number(r.account_id),
|
|
accountAlias: accResult.rows[0]?.alias ?? null,
|
|
amountSigned: Number(r.amount_signed),
|
|
commission: Number(r.commission),
|
|
description: r.description,
|
|
direction: r.direction,
|
|
categoryId: r.category_id != null ? Number(r.category_id) : null,
|
|
categoryName: catResult.rows[0]?.name ?? null,
|
|
isCategoryConfirmed: r.is_category_confirmed,
|
|
comment: r.comment ?? null,
|
|
};
|
|
}
|
|
|
|
export async function clearAllTransactions(): Promise<{ deleted: number }> {
|
|
const result = await pool.query('DELETE FROM transactions RETURNING id');
|
|
return { deleted: result.rowCount ?? 0 };
|
|
}
|