feat: creates backend for the project

This commit is contained in:
vakabunga
2026-03-02 00:32:37 +03:00
parent 9d12702688
commit 4d67636633
24 changed files with 1735 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
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,
};
}