Compare commits
14 Commits
feature/tr
...
fix/vtb-sa
| Author | SHA1 | Date | |
|---|---|---|---|
| fab929fd68 | |||
| c4ce2b9d6b | |||
| c4f681c9b0 | |||
| dd21e20dc6 | |||
| 29c4acd0a9 | |||
| 8939843462 | |||
| ce162855f9 | |||
| 7671ab76b2 | |||
| cce2ddcf41 | |||
| 7154e8f2ea | |||
| d86624b9ef | |||
| 66c04b0618 | |||
| 24b8ed8261 | |||
| efc9854064 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,5 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Backend 0.10.3] - 2026-08-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Treat VTB savings-account deposits and closures as internal transfers when importing PDF-to-JSON statements.
|
||||||
|
|
||||||
|
## [Backend 0.10.2] - 2026-08-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Analytics endpoints now reject invalid account and category filter IDs with a validation error.
|
||||||
|
|
||||||
|
## [Frontend 0.11.1] - 2026-08-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Preserve the explicit «Без категории» analytics filter value when requesting data.
|
||||||
|
|
||||||
|
## [Frontend 0.11.0 / Backend 0.10.1 / Shared 0.5.0] - 2026-08-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Corrected analytics period boundaries for partial week/month ranges and added a usable «Без категории» filter with SQL coverage.
|
||||||
|
|
||||||
## [Backend 0.9.2] - 2026-08-20
|
## [Backend 0.9.2] - 2026-08-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/backend",
|
"name": "@family-budget/backend",
|
||||||
"version": "0.9.2",
|
"version": "0.10.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
"migrate": "tsx src/db/migrate.ts",
|
"migrate": "tsx src/db/migrate.ts",
|
||||||
"migrate:prod": "node dist/db/migrate.js",
|
"migrate:prod": "node dist/db/migrate.js",
|
||||||
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
|
"test:analytics": "tsx src/services/analyticsSemantics.test.ts",
|
||||||
|
"test:analytics:query": "tsx src/routes/analytics.test.ts",
|
||||||
"test:portfolio": "tsx src/services/portfolio.test.ts",
|
"test:portfolio": "tsx src/services/portfolio.test.ts",
|
||||||
"test:portfolio:db": "NODE_ENV=test tsx src/services/portfolio.integration.test.ts",
|
"test:portfolio:db": "NODE_ENV=test tsx src/services/portfolio.integration.test.ts",
|
||||||
"test:transactions": "tsx src/services/transactions.test.ts",
|
"test:transactions": "tsx src/services/transactions.test.ts",
|
||||||
"test:analytics:db": "NODE_ENV=test tsx src/services/analytics.integration.test.ts",
|
"test:analytics:db": "NODE_ENV=test tsx src/services/analytics.integration.test.ts",
|
||||||
"test:import:db": "NODE_ENV=test tsx src/services/import.integration.test.ts",
|
"test:import:db": "NODE_ENV=test tsx src/services/import.integration.test.ts",
|
||||||
|
"test:import:direction": "tsx src/services/import.test.ts",
|
||||||
"test:llm": "tsx src/scripts/testLlm.ts"
|
"test:llm": "tsx src/scripts/testLlm.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
12
backend/src/routes/analytics.test.ts
Normal file
12
backend/src/routes/analytics.test.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { parseOptionalId } from './analytics';
|
||||||
|
|
||||||
|
assert.equal(parseOptionalId(undefined, 0), undefined);
|
||||||
|
assert.equal(parseOptionalId('0', 0), 0);
|
||||||
|
assert.equal(parseOptionalId('1', 1), 1);
|
||||||
|
assert.equal(parseOptionalId('0', 1), null);
|
||||||
|
assert.equal(parseOptionalId('abc', 0), null);
|
||||||
|
assert.equal(parseOptionalId('1.5', 0), null);
|
||||||
|
assert.equal(parseOptionalId(['1'], 0), null);
|
||||||
|
|
||||||
|
console.log('analytics query validation: OK');
|
||||||
@@ -5,10 +5,34 @@ import type { Granularity } from '@family-budget/shared';
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
export function parseOptionalId(value: unknown, minimum: number): number | null | undefined {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
if (typeof value !== 'string' || !/^\d+$/.test(value)) return null;
|
||||||
|
|
||||||
|
const id = Number(value);
|
||||||
|
return Number.isSafeInteger(id) && id >= minimum ? id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
const accountId = parseOptionalId(req.query.accountId, 1);
|
||||||
|
if (accountId === null) {
|
||||||
|
res.status(400).json({ error: 'BAD_REQUEST', message: 'accountId must be a positive integer' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryId = parseOptionalId(req.query.categoryId, 0);
|
||||||
|
if (categoryId === null) {
|
||||||
|
res.status(400).json({ error: 'BAD_REQUEST', message: 'categoryId must be a non-negative integer' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/summary',
|
'/summary',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
const { from, to, accountId, onlyConfirmed } = req.query;
|
const { from, to, accountId, categoryId, onlyConfirmed } = req.query;
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' });
|
res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' });
|
||||||
return;
|
return;
|
||||||
@@ -18,6 +42,7 @@ router.get(
|
|||||||
from: from as string,
|
from: from as string,
|
||||||
to: to as string,
|
to: to as string,
|
||||||
accountId: accountId ? Number(accountId) : undefined,
|
accountId: accountId ? Number(accountId) : undefined,
|
||||||
|
categoryId: categoryId !== undefined ? Number(categoryId) : undefined,
|
||||||
onlyConfirmed: onlyConfirmed === 'true',
|
onlyConfirmed: onlyConfirmed === 'true',
|
||||||
});
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
@@ -27,7 +52,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
'/by-category',
|
'/by-category',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
const { from, to, accountId, onlyConfirmed } = req.query;
|
const { from, to, accountId, categoryId, onlyConfirmed } = req.query;
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' });
|
res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' });
|
||||||
return;
|
return;
|
||||||
@@ -37,6 +62,7 @@ router.get(
|
|||||||
from: from as string,
|
from: from as string,
|
||||||
to: to as string,
|
to: to as string,
|
||||||
accountId: accountId ? Number(accountId) : undefined,
|
accountId: accountId ? Number(accountId) : undefined,
|
||||||
|
categoryId: categoryId !== undefined ? Number(categoryId) : undefined,
|
||||||
onlyConfirmed: onlyConfirmed === 'true',
|
onlyConfirmed: onlyConfirmed === 'true',
|
||||||
});
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
@@ -62,7 +88,7 @@ router.get(
|
|||||||
from: from as string,
|
from: from as string,
|
||||||
to: to as string,
|
to: to as string,
|
||||||
accountId: accountId ? Number(accountId) : undefined,
|
accountId: accountId ? Number(accountId) : undefined,
|
||||||
categoryId: categoryId ? Number(categoryId) : undefined,
|
categoryId: categoryId !== undefined ? Number(categoryId) : undefined,
|
||||||
onlyConfirmed: onlyConfirmed === 'true',
|
onlyConfirmed: onlyConfirmed === 'true',
|
||||||
granularity: granularity as Granularity,
|
granularity: granularity as Granularity,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,15 +28,33 @@ async function testQueries(): Promise<void> {
|
|||||||
);
|
);
|
||||||
await insert(otherAccountId, '2026-07-01T12:00:00+03:00', -99_000, categoryId.expense, 'analytics-test-6');
|
await insert(otherAccountId, '2026-07-01T12:00:00+03:00', -99_000, categoryId.expense, 'analytics-test-6');
|
||||||
await insert(accountId, '2026-08-01T12:00:00+03:00', -7_000, categoryId.expense, 'analytics-test-7');
|
await insert(accountId, '2026-08-01T12:00:00+03:00', -7_000, categoryId.expense, 'analytics-test-7');
|
||||||
|
await client.query(
|
||||||
|
"INSERT INTO transactions (account_id, operation_at, amount_signed, commission, description, direction, fingerprint, category_id, is_category_confirmed) VALUES ($1, '2026-07-07T12:00:00+03:00', -2_500, 0, 'uncategorized', 'expense', 'analytics-test-uncategorized', NULL, FALSE)",
|
||||||
|
[accountId],
|
||||||
|
);
|
||||||
|
|
||||||
const params = { from: '2026-07-01', to: '2026-07-31', accountId, onlyConfirmed: true };
|
const params = { from: '2026-07-01', to: '2026-07-31', accountId, onlyConfirmed: true };
|
||||||
const summary = await getSummary(params, client);
|
const summary = await getSummary(params, client);
|
||||||
assert.deepEqual({ expense: summary.totalExpense, income: summary.totalIncome, net: summary.net, transferOut: summary.transferOutflow, interest: summary.interestIncome }, { expense: 6_000, income: 15_000, net: 9_000, transferOut: 3_000, interest: 1_500 });
|
assert.deepEqual({ expense: summary.totalExpense, income: summary.totalIncome, net: summary.net, transferOut: summary.transferOutflow, interest: summary.interestIncome }, { expense: 6_000, income: 15_000, net: 9_000, transferOut: 3_000, interest: 1_500 });
|
||||||
|
const categorySummary = await getSummary({ ...params, categoryId: categoryId.expense }, client);
|
||||||
|
assert.deepEqual({ expense: categorySummary.totalExpense, income: categorySummary.totalIncome }, { expense: 6_000, income: 0 });
|
||||||
|
const uncategorized = await getSummary({ ...params, categoryId: 0, onlyConfirmed: false }, client);
|
||||||
|
assert.equal(uncategorized.totalExpense, 2_500);
|
||||||
|
assert.equal((await getSummary({ ...params, categoryId: 0, onlyConfirmed: true }, client)).totalExpense, 0);
|
||||||
|
assert.deepEqual((await getByCategory({ ...params, categoryId: 0, onlyConfirmed: false }, client)).map((item) => item.amount), [2_500]);
|
||||||
|
assert.deepEqual((await getByCategory({ ...params, categoryId: 0, onlyConfirmed: true }, client)), []);
|
||||||
assert.equal((await getSummary({ ...params, from: '2026-08-01', to: '2026-08-31' }, client)).totalExpense, 7_000);
|
assert.equal((await getSummary({ ...params, from: '2026-08-01', to: '2026-08-31' }, client)).totalExpense, 7_000);
|
||||||
assert.deepEqual((await getByCategory(params, client)).map((item) => item.amount), [6_000]);
|
assert.deepEqual((await getByCategory(params, client)).map((item) => item.amount), [6_000]);
|
||||||
const timeseries = await getTimeseries({ ...params, granularity: 'month' }, client);
|
const timeseries = await getTimeseries({ ...params, granularity: 'month' }, client);
|
||||||
assert.equal(timeseries[0].expenseAmount, 6_000);
|
assert.equal(timeseries[0].expenseAmount, 6_000);
|
||||||
assert.equal(timeseries[0].incomeAmount, 15_000);
|
assert.equal(timeseries[0].incomeAmount, 15_000);
|
||||||
|
const partial = await getTimeseries({ ...params, from: '2026-07-02', to: '2026-07-03', granularity: 'month' }, client);
|
||||||
|
assert.equal(partial[0].expenseAmount, 0);
|
||||||
|
assert.equal(partial[0].incomeAmount, 20_000);
|
||||||
|
const uncategorizedSeries = await getTimeseries({ ...params, categoryId: 0, onlyConfirmed: false, granularity: 'month' }, client);
|
||||||
|
assert.equal(uncategorizedSeries[0].expenseAmount, 2_500);
|
||||||
|
const confirmedUncategorizedSeries = await getTimeseries({ ...params, categoryId: 0, onlyConfirmed: true, granularity: 'month' }, client);
|
||||||
|
assert.equal(confirmedUncategorizedSeries[0].expenseAmount, 0);
|
||||||
} finally {
|
} finally {
|
||||||
await client.query('ROLLBACK');
|
await client.query('ROLLBACK');
|
||||||
client.release();
|
client.release();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface BaseParams {
|
|||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
accountId?: number;
|
accountId?: number;
|
||||||
|
categoryId?: number;
|
||||||
onlyConfirmed?: boolean;
|
onlyConfirmed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +62,13 @@ function buildBaseConditions(
|
|||||||
values.push(params.accountId);
|
values.push(params.accountId);
|
||||||
idx++;
|
idx++;
|
||||||
}
|
}
|
||||||
|
if (params.categoryId != null) {
|
||||||
|
conditions.push(params.categoryId === 0 ? 't.category_id IS NULL' : `t.category_id = $${idx}`);
|
||||||
|
if (params.categoryId !== 0) {
|
||||||
|
values.push(params.categoryId);
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (params.onlyConfirmed) {
|
if (params.onlyConfirmed) {
|
||||||
conditions.push('t.is_category_confirmed = TRUE');
|
conditions.push('t.is_category_confirmed = TRUE');
|
||||||
}
|
}
|
||||||
@@ -196,6 +204,8 @@ export async function getTimeseries(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const txConditions: string[] = [
|
const txConditions: string[] = [
|
||||||
|
't.operation_at::date >= $1::date',
|
||||||
|
't.operation_at::date <= $2::date',
|
||||||
't.operation_at::date >= p.period_start',
|
't.operation_at::date >= p.period_start',
|
||||||
't.operation_at::date <= p.period_end',
|
't.operation_at::date <= p.period_end',
|
||||||
];
|
];
|
||||||
@@ -207,9 +217,12 @@ export async function getTimeseries(
|
|||||||
values.push(params.accountId);
|
values.push(params.accountId);
|
||||||
}
|
}
|
||||||
if (params.categoryId != null) {
|
if (params.categoryId != null) {
|
||||||
|
if (params.categoryId === 0) txConditions.push('t.category_id IS NULL');
|
||||||
|
else {
|
||||||
txConditions.push(`t.category_id = $${idx++}`);
|
txConditions.push(`t.category_id = $${idx++}`);
|
||||||
values.push(params.categoryId);
|
values.push(params.categoryId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (params.onlyConfirmed) {
|
if (params.onlyConfirmed) {
|
||||||
txConditions.push('t.is_category_confirmed = TRUE');
|
txConditions.push('t.is_category_confirmed = TRUE');
|
||||||
}
|
}
|
||||||
|
|||||||
10
backend/src/services/import.test.ts
Normal file
10
backend/src/services/import.test.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { determineDirection } from './import';
|
||||||
|
|
||||||
|
assert.equal(determineDirection(1, 'Перечисление средств на счет N 123 со счета N 456'), 'transfer');
|
||||||
|
assert.equal(determineDirection(1, 'Перечисление средств на вклад N 123'), 'transfer');
|
||||||
|
assert.equal(determineDirection(-1, 'Перечисление суммы вклада при закрытии'), 'transfer');
|
||||||
|
assert.equal(determineDirection(-1, 'Оплата покупки'), 'expense');
|
||||||
|
assert.equal(determineDirection(1, 'Выплата процентов'), 'income');
|
||||||
|
|
||||||
|
console.log('import direction: OK');
|
||||||
@@ -6,6 +6,9 @@ import type { StatementFile, ImportStatementResponse } from '@family-budget/shar
|
|||||||
const TRANSFER_PHRASES = [
|
const TRANSFER_PHRASES = [
|
||||||
'перевод между своими счетами',
|
'перевод между своими счетами',
|
||||||
'перевод средств на счет',
|
'перевод средств на счет',
|
||||||
|
'перечисление средств на счет',
|
||||||
|
'перечисление средств на вклад',
|
||||||
|
'перечисление суммы вклада при закрытии',
|
||||||
'внутри втб',
|
'внутри втб',
|
||||||
];
|
];
|
||||||
const CASHBACK_KEYWORD = 'зачисление';
|
const CASHBACK_KEYWORD = 'зачисление';
|
||||||
@@ -31,7 +34,7 @@ function computeFingerprint(
|
|||||||
return `sha256:${hash}`;
|
return `sha256:${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function determineDirection(amountSigned: number, description: string): string {
|
export function determineDirection(amountSigned: number, description: string): string {
|
||||||
const lower = description.toLowerCase();
|
const lower = description.toLowerCase();
|
||||||
for (const phrase of TRANSFER_PHRASES) {
|
for (const phrase of TRANSFER_PHRASES) {
|
||||||
if (lower.includes(phrase)) return 'transfer';
|
if (lower.includes(phrase)) return 'transfer';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/frontend",
|
"name": "@family-budget/frontend",
|
||||||
"version": "0.10.2",
|
"version": "0.11.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import type {
|
import type {
|
||||||
Account,
|
Account,
|
||||||
|
Category,
|
||||||
AnalyticsSummaryResponse,
|
AnalyticsSummaryResponse,
|
||||||
ByCategoryItem,
|
ByCategoryItem,
|
||||||
TimeseriesItem,
|
TimeseriesItem,
|
||||||
Granularity,
|
Granularity,
|
||||||
} from '@family-budget/shared';
|
} from '@family-budget/shared';
|
||||||
import { getAccounts } from '../api/accounts';
|
import { getAccounts } from '../api/accounts';
|
||||||
|
import { getCategories } from '../api/categories';
|
||||||
import { getSummary, getByCategory, getTimeseries } from '../api/analytics';
|
import { getSummary, getByCategory, getTimeseries } from '../api/analytics';
|
||||||
import {
|
import {
|
||||||
PeriodSelector,
|
PeriodSelector,
|
||||||
@@ -29,8 +31,10 @@ function getDefaultPeriod(): PeriodState {
|
|||||||
export function AnalyticsPage() {
|
export function AnalyticsPage() {
|
||||||
const [period, setPeriod] = useState<PeriodState>(getDefaultPeriod);
|
const [period, setPeriod] = useState<PeriodState>(getDefaultPeriod);
|
||||||
const [accountId, setAccountId] = useState<string>('');
|
const [accountId, setAccountId] = useState<string>('');
|
||||||
|
const [categoryId, setCategoryId] = useState<string>('');
|
||||||
const [onlyConfirmed, setOnlyConfirmed] = useState(false);
|
const [onlyConfirmed, setOnlyConfirmed] = useState(false);
|
||||||
const [accounts, setAccounts] = useState<Account[]>([]);
|
const [accounts, setAccounts] = useState<Account[]>([]);
|
||||||
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [summary, setSummary] = useState<AnalyticsSummaryResponse | null>(
|
const [summary, setSummary] = useState<AnalyticsSummaryResponse | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -40,6 +44,7 @@ export function AnalyticsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAccounts().then(setAccounts).catch(() => {});
|
getAccounts().then(setAccounts).catch(() => {});
|
||||||
|
getCategories({ isActive: true }).then(setCategories).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchAll = useCallback(async () => {
|
const fetchAll = useCallback(async () => {
|
||||||
@@ -63,6 +68,7 @@ export function AnalyticsPage() {
|
|||||||
from: period.from,
|
from: period.from,
|
||||||
to: period.to,
|
to: period.to,
|
||||||
...(accountId ? { accountId: Number(accountId) } : {}),
|
...(accountId ? { accountId: Number(accountId) } : {}),
|
||||||
|
...(categoryId !== '' ? { categoryId: Number(categoryId) } : {}),
|
||||||
...(onlyConfirmed ? { onlyConfirmed: true } : {}),
|
...(onlyConfirmed ? { onlyConfirmed: true } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,7 +86,7 @@ export function AnalyticsPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [period, accountId, onlyConfirmed]);
|
}, [period, accountId, categoryId, onlyConfirmed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAll();
|
fetchAll();
|
||||||
@@ -122,6 +128,14 @@ export function AnalyticsPage() {
|
|||||||
Только подтверждённые
|
Только подтверждённые
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="field__label">Категория</label>
|
||||||
|
<select value={categoryId} onChange={(e) => setCategoryId(e.target.value)}>
|
||||||
|
<option value="">Все категории</option>
|
||||||
|
<option value="0">Без категории</option>
|
||||||
|
{categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@family-budget/shared",
|
"name": "@family-budget/shared",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface AnalyticsSummaryParams {
|
|||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
accountId?: number;
|
accountId?: number;
|
||||||
|
categoryId?: number;
|
||||||
onlyConfirmed?: boolean;
|
onlyConfirmed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ export interface ByCategoryParams {
|
|||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
accountId?: number;
|
accountId?: number;
|
||||||
|
categoryId?: number;
|
||||||
onlyConfirmed?: boolean;
|
onlyConfirmed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user