From 24b8ed826107aa67703802f066319a82a852253b Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 21 Aug 2026 00:08:59 +0300 Subject: [PATCH] feat: complete analytics category filtering --- CHANGELOG.md | 6 ++++++ backend/package.json | 2 +- backend/src/routes/analytics.ts | 6 ++++-- .../src/services/analytics.integration.test.ts | 2 ++ backend/src/services/analytics.ts | 6 ++++++ frontend/package.json | 2 +- frontend/src/pages/AnalyticsPage.tsx | 15 ++++++++++++++- shared/package.json | 2 +- shared/src/types/analytics.ts | 2 ++ 9 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2fc69..440a407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Frontend 0.11.0 / Backend 0.10.0 / Shared 0.5.0] - 2026-08-21 + +### Added + +- Added category filtering to analytics summary and category breakdown, including savings-account interest handling in the filtered results. + ## [Backend 0.9.2] - 2026-08-20 ### Added diff --git a/backend/package.json b/backend/package.json index abac951..c0f9316 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "@family-budget/backend", - "version": "0.9.2", + "version": "0.10.0", "private": true, "scripts": { "dev": "tsx watch src/app.ts", diff --git a/backend/src/routes/analytics.ts b/backend/src/routes/analytics.ts index c5608ed..4e83bf6 100644 --- a/backend/src/routes/analytics.ts +++ b/backend/src/routes/analytics.ts @@ -8,7 +8,7 @@ const router = Router(); router.get( '/summary', asyncHandler(async (req, res) => { - const { from, to, accountId, onlyConfirmed } = req.query; + const { from, to, accountId, categoryId, onlyConfirmed } = req.query; if (!from || !to) { res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' }); return; @@ -18,6 +18,7 @@ router.get( from: from as string, to: to as string, accountId: accountId ? Number(accountId) : undefined, + categoryId: categoryId ? Number(categoryId) : undefined, onlyConfirmed: onlyConfirmed === 'true', }); res.json(result); @@ -27,7 +28,7 @@ router.get( router.get( '/by-category', asyncHandler(async (req, res) => { - const { from, to, accountId, onlyConfirmed } = req.query; + const { from, to, accountId, categoryId, onlyConfirmed } = req.query; if (!from || !to) { res.status(400).json({ error: 'BAD_REQUEST', message: 'from and to are required' }); return; @@ -37,6 +38,7 @@ router.get( from: from as string, to: to as string, accountId: accountId ? Number(accountId) : undefined, + categoryId: categoryId ? Number(categoryId) : undefined, onlyConfirmed: onlyConfirmed === 'true', }); res.json(result); diff --git a/backend/src/services/analytics.integration.test.ts b/backend/src/services/analytics.integration.test.ts index cfbd548..3df48d3 100644 --- a/backend/src/services/analytics.integration.test.ts +++ b/backend/src/services/analytics.integration.test.ts @@ -32,6 +32,8 @@ async function testQueries(): Promise { const params = { from: '2026-07-01', to: '2026-07-31', accountId, onlyConfirmed: true }; 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 }); + const categorySummary = await getSummary({ ...params, categoryId: categoryId.expense }, client); + assert.deepEqual({ expense: categorySummary.totalExpense, income: categorySummary.totalIncome }, { expense: 6_000, income: 0 }); 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]); const timeseries = await getTimeseries({ ...params, granularity: 'month' }, client); diff --git a/backend/src/services/analytics.ts b/backend/src/services/analytics.ts index fdbf0e2..1b92394 100644 --- a/backend/src/services/analytics.ts +++ b/backend/src/services/analytics.ts @@ -14,6 +14,7 @@ interface BaseParams { from: string; to: string; accountId?: number; + categoryId?: number; onlyConfirmed?: boolean; } @@ -61,6 +62,11 @@ function buildBaseConditions( values.push(params.accountId); idx++; } + if (params.categoryId != null) { + conditions.push(`t.category_id = $${idx}`); + values.push(params.categoryId); + idx++; + } if (params.onlyConfirmed) { conditions.push('t.is_category_confirmed = TRUE'); } diff --git a/frontend/package.json b/frontend/package.json index 261e832..93ec54e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "@family-budget/frontend", - "version": "0.10.2", + "version": "0.11.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/pages/AnalyticsPage.tsx b/frontend/src/pages/AnalyticsPage.tsx index 5783aac..b9fe2ae 100644 --- a/frontend/src/pages/AnalyticsPage.tsx +++ b/frontend/src/pages/AnalyticsPage.tsx @@ -1,12 +1,14 @@ import { useState, useEffect, useCallback } from 'react'; import type { Account, + Category, AnalyticsSummaryResponse, ByCategoryItem, TimeseriesItem, Granularity, } from '@family-budget/shared'; import { getAccounts } from '../api/accounts'; +import { getCategories } from '../api/categories'; import { getSummary, getByCategory, getTimeseries } from '../api/analytics'; import { PeriodSelector, @@ -29,8 +31,10 @@ function getDefaultPeriod(): PeriodState { export function AnalyticsPage() { const [period, setPeriod] = useState(getDefaultPeriod); const [accountId, setAccountId] = useState(''); + const [categoryId, setCategoryId] = useState(''); const [onlyConfirmed, setOnlyConfirmed] = useState(false); const [accounts, setAccounts] = useState([]); + const [categories, setCategories] = useState([]); const [summary, setSummary] = useState( null, ); @@ -40,6 +44,7 @@ export function AnalyticsPage() { useEffect(() => { getAccounts().then(setAccounts).catch(() => {}); + getCategories({ isActive: true }).then(setCategories).catch(() => {}); }, []); const fetchAll = useCallback(async () => { @@ -63,6 +68,7 @@ export function AnalyticsPage() { from: period.from, to: period.to, ...(accountId ? { accountId: Number(accountId) } : {}), + ...(categoryId ? { categoryId: Number(categoryId) } : {}), ...(onlyConfirmed ? { onlyConfirmed: true } : {}), }; @@ -80,7 +86,7 @@ export function AnalyticsPage() { } finally { setLoading(false); } - }, [period, accountId, onlyConfirmed]); + }, [period, accountId, categoryId, onlyConfirmed]); useEffect(() => { fetchAll(); @@ -112,6 +118,13 @@ export function AnalyticsPage() { ))} +
+ + +