From b00e0d2d337c67f9f334a290da18c563755aac38 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Fri, 31 Jul 2026 19:53:16 +0200 Subject: [PATCH 1/4] feat: consider input filter to sum transations --- CHANGELOG.md | 27 +++++++++++++++++++-------- frontend/src/pages/Transactions.jsx | 19 +++++++++++-------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b1186..6d66521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,25 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## frontend:v2026.07.14.? & backend:v2026.07.14.? - 2026-07-14 +## 2026-07-31 -## Added +### Changed +- Category and input filter in the transaction page now show the sum of all matching transactions. + +--- + +## 2026-07-14 + +### Added - Scroll-linked header shrink effect in a progressive style. [Issue #4](https://lightroasted.vps-kinghost.net/rmcampos/ledger-finance/issues/4) - Page transition on route change. [Issue #5](https://lightroasted.vps-kinghost.net/rmcampos/ledger-finance/issues/5) - Sidebar active-link indicator now slide instead of snap. [Issue #6](https://lightroasted.vps-kinghost.net/rmcampos/ledger-finance/issues/6) - Today and Next two days transactions in the overview page. [Issue #1](https://lightroasted.vps-kinghost.net/rmcampos/ledger-finance/issues/1) -## Changed +### Changed - Headers in all pages to scroll smoothier. [Issue #3](https://lightroasted.vps-kinghost.net/rmcampos/ledger-finance/issues/3). -## Fixed +### Fixed - Apple and iOS PWA icon when installed at Home Screen. [Issue #2](https://lightroasted.vps-kinghost.net/rmcampos/ledger-finance/issues/2) -### Docker images -- `docker.io/rmcampos/ledger-backend:v2026.07.14.?` -- `docker.io/rmcampos/ledger-frontend:v2026.07.14.?` +```bash +# Docker images +docker.io/rmcampos/ledger-backend:v2026.07.14.? +docker.io/rmcampos/ledger-frontend:v2026.07.14.? +``` -## frontend:v2026.07.12.102 & backend:v2026.07.12.104 - 2026-07-12 +--- + +## 2026-07-12 ## Added - Predicted balances for the next three months in the transactions page. diff --git a/frontend/src/pages/Transactions.jsx b/frontend/src/pages/Transactions.jsx index ad23840..3ba24bd 100644 --- a/frontend/src/pages/Transactions.jsx +++ b/frontend/src/pages/Transactions.jsx @@ -487,10 +487,11 @@ export default function Transactions() { }) : []; const showPredicted = predictedMonths.length > 0; + const hasActiveFilter = Boolean(filterCategoryId || search.trim()); const balanceColClass = showPredicted - ? (filterCategoryId ? 'col-md-5' : 'col-md-8') - : (filterCategoryId ? 'col-md-7' : 'col-md-12'); - const predictedColClass = filterCategoryId ? 'col-md-3' : 'col-md-4'; + ? (hasActiveFilter ? 'col-md-5' : 'col-md-8') + : (hasActiveFilter ? 'col-md-7' : 'col-md-12'); + const predictedColClass = hasActiveFilter ? 'col-md-3' : 'col-md-4'; const categoryColClass = showPredicted ? 'col-md-4' : 'col-md-5'; const filteredCategoryName = categories.find((c) => String(c.id) === filterCategoryId)?.name; @@ -525,7 +526,7 @@ export default function Transactions() { return Array.from(map.entries()); }, [filteredFlat]); - const filteredCategoryTotal = filteredFlat.reduce((sum, t) => sum + Number(t.amount), 0); + const filteredTotal = filteredFlat.reduce((sum, t) => sum + Number(t.amount), 0); const handleExport = () => { downloadCsv(toCsv(filteredFlat), `transactions-${todayIso()}.csv`); @@ -727,14 +728,16 @@ export default function Transactions() { )} - {filterCategoryId && ( + {hasActiveFilter && (
-
{filteredCategoryName || 'Category'} total
+
+ {filterCategoryId ? `${filteredCategoryName} total` : 'Filtered total'} +
- {money(filteredCategoryTotal, { signed: true })} + {money(filteredTotal, { signed: true })}
Sum of currently filtered transactions. -- 2.54.0 From 21d546ce5cc830106c95bee8012546e468e8d9af Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Fri, 31 Jul 2026 20:18:44 +0200 Subject: [PATCH 2/4] feat: add monthly totals to transactions --- CHANGELOG.md | 5 ++- frontend/src/pages/Transactions.jsx | 58 +++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d66521..8cbbada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 2026-07-31 +### Added +- Monthly totals in the transactions page. + ### Changed -- Category and input filter in the transaction page now show the sum of all matching transactions. +- Category and input filter in the transactions page now show the sum of all matching transactions. --- diff --git a/frontend/src/pages/Transactions.jsx b/frontend/src/pages/Transactions.jsx index 3ba24bd..7cd3cad 100644 --- a/frontend/src/pages/Transactions.jsx +++ b/frontend/src/pages/Transactions.jsx @@ -488,11 +488,15 @@ export default function Transactions() { : []; const showPredicted = predictedMonths.length > 0; const hasActiveFilter = Boolean(filterCategoryId || search.trim()); - const balanceColClass = showPredicted - ? (hasActiveFilter ? 'col-md-5' : 'col-md-8') - : (hasActiveFilter ? 'col-md-7' : 'col-md-12'); - const predictedColClass = hasActiveFilter ? 'col-md-3' : 'col-md-4'; - const categoryColClass = showPredicted ? 'col-md-4' : 'col-md-5'; + const showMonthlyTotals = filterRange === 'this-month'; + + const visibleCardCount = + 1 + (showMonthlyTotals ? 1 : 0) + (showPredicted ? 1 : 0) + (hasActiveFilter ? 1 : 0); + const topCardClass = + visibleCardCount === 4 ? 'col-md-3' : + visibleCardCount === 3 ? 'col-md-4' : + visibleCardCount === 2 ? 'col-md-6' : + 'col-md-12'; const filteredCategoryName = categories.find((c) => String(c.id) === filterCategoryId)?.name; @@ -528,6 +532,25 @@ export default function Transactions() { const filteredTotal = filteredFlat.reduce((sum, t) => sum + Number(t.amount), 0); + const monthlyFlat = useMemo(() => { + const { start, end } = rangeBounds(filterRange, filterStartDate, filterEndDate, monthOffset); + return txnQueries + .flatMap((q) => q.data || []) + .filter((t) => !filterAccountId || String(t.account?.id) === filterAccountId) + .filter((t) => !start || parseLocalDate(t.occurredOn) >= start) + .filter((t) => !end || parseLocalDate(t.occurredOn) <= end) + .sort((a, b) => parseLocalDate(b.occurredOn) - parseLocalDate(a.occurredOn)); + }, [txnQueries, filterAccountId, filterRange, filterStartDate, filterEndDate, monthOffset]); + + const monthlyCredits = monthlyFlat.reduce( + (sum, t) => sum + (Number(t.amount) > 0 ? Number(t.amount) : 0), + 0 + ); + const monthlyDebits = monthlyFlat.reduce( + (sum, t) => sum + (Number(t.amount) < 0 ? Number(t.amount) : 0), + 0 + ); + const handleExport = () => { downloadCsv(toCsv(filteredFlat), `transactions-${todayIso()}.csv`); }; @@ -697,7 +720,7 @@ export default function Transactions() {
-
+
Current balance @@ -713,8 +736,27 @@ export default function Transactions() {
)}
+ {showMonthlyTotals && ( +
+
Monthly totals
+
+
+ Credits + + {money(monthlyCredits, { signed: true })} + +
+
+ Debits + + {money(monthlyDebits, { signed: true })} + +
+
+
+ )} {showPredicted && ( -
+
Predicted
{predictedMonths.map((m) => ( @@ -729,7 +771,7 @@ export default function Transactions() {
)} {hasActiveFilter && ( -
+
{filterCategoryId ? `${filteredCategoryName} total` : 'Filtered total'}
-- 2.54.0 From 4c735f397bd7b1338a584929101f9657c1bd9954 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Fri, 31 Jul 2026 20:45:41 +0200 Subject: [PATCH 3/4] feat: load transactions from the budgets page --- CHANGELOG.md | 1 + .../com/ledger/resource/BudgetResource.java | 18 ++++ frontend/src/api/ledger.js | 2 + .../src/components/BudgetTransactionList.jsx | 92 +++++++++++++++++++ frontend/src/pages/Budgets.jsx | 42 ++++++++- 5 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/BudgetTransactionList.jsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbbada..185f10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Monthly totals in the transactions page. +- Click to load transactions in the budgets page. ### Changed - Category and input filter in the transactions page now show the sum of all matching transactions. diff --git a/backend/src/main/java/com/ledger/resource/BudgetResource.java b/backend/src/main/java/com/ledger/resource/BudgetResource.java index 1c98f81..2a6f81f 100644 --- a/backend/src/main/java/com/ledger/resource/BudgetResource.java +++ b/backend/src/main/java/com/ledger/resource/BudgetResource.java @@ -3,8 +3,10 @@ package com.ledger.resource; import com.ledger.entity.Budget; import com.ledger.dto.response.CategorySpendResponse; import com.ledger.entity.Category; +import com.ledger.entity.Transaction; import com.ledger.security.CurrentUserService; import com.ledger.entity.User; +import io.quarkus.panache.common.Sort; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; @@ -40,6 +42,22 @@ public class BudgetResource { * Budget row — so it always reflects the live ledger. Only negative (expense) transactions are * summed; positive amounts (income, transfers in) are excluded from "spent". */ + @GET + @Path("/month/{yearMonth}/category/{categoryId}/transactions") + public List transactionsForCategory( + @PathParam("yearMonth") String yearMonth, @PathParam("categoryId") Long categoryId) { + User user = currentUser.require(); + LocalDate start = LocalDate.parse(yearMonth + "-01"); + LocalDate end = start.plusMonths(1).minusDays(1); + return Transaction.list( + "account.user.id = ?1 and category.id = ?2 and occurredOn between ?3 and ?4", + Sort.descending("occurredOn").and("id"), + user.id, + categoryId, + start, + end); + } + @GET @Path("/month/{yearMonth}/spend") public List spendForMonth(@PathParam("yearMonth") String yearMonth) { diff --git a/frontend/src/api/ledger.js b/frontend/src/api/ledger.js index e88f65d..b6d791e 100644 --- a/frontend/src/api/ledger.js +++ b/frontend/src/api/ledger.js @@ -56,6 +56,8 @@ export const BudgetsApi = { client.get(`/budgets/month/${yearMonth}`).then((r) => r.data), spendForMonth: (yearMonth) => client.get(`/budgets/month/${yearMonth}/spend`).then((r) => r.data), + transactionsForBudget: (yearMonth, categoryId) => + client.get(`/budgets/month/${yearMonth}/category/${categoryId}/transactions`).then((r) => r.data), upsert: (payload) => client.post('/budgets', payload).then((r) => r.data), remove: (id) => client.delete(`/budgets/${id}`), }; diff --git a/frontend/src/components/BudgetTransactionList.jsx b/frontend/src/components/BudgetTransactionList.jsx new file mode 100644 index 0000000..3901cf6 --- /dev/null +++ b/frontend/src/components/BudgetTransactionList.jsx @@ -0,0 +1,92 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { BudgetsApi } from '../api/ledger'; +import { parseLocalDate } from '../utils/date'; + +function money(amount) { + const value = Math.abs(Number(amount)).toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + const sign = Number(amount) < 0 ? '-' : ''; + return `${sign}$${value}`; +} + +function dayLabel(dateStr) { + const date = parseLocalDate(dateStr); + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + const sameDay = (a, b) => a.toDateString() === b.toDateString(); + if (sameDay(date, today)) return `Today · ${date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; + if (sameDay(date, yesterday)) + return `Yesterday · ${date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +} + +export default function BudgetTransactionList({ yearMonth, categoryId }) { + const { data = [], isLoading } = useQuery({ + queryKey: ['budget-transactions', yearMonth, categoryId], + queryFn: () => BudgetsApi.transactionsForBudget(yearMonth, categoryId), + enabled: !!categoryId, + }); + + const grouped = useMemo(() => { + const map = new Map(); + for (const t of data) { + const label = dayLabel(t.occurredOn); + if (!map.has(label)) map.set(label, []); + map.get(label).push(t); + } + return Array.from(map.entries()); + }, [data]); + + if (isLoading) return
Loading…
; + if (grouped.length === 0) return
No transactions.
; + + return ( +
+ {grouped.map(([label, txns]) => ( +
+
+
{label}
+
+ {txns.map((t) => ( +
+
+
+ {t.description} + {t.seriesInfo && ( + + {t.seriesInfo} + + )} +
+
+ {t.account?.name || 'Unknown account'} + {t.occurredOn && ( + <> + + {t.occurredOn} + + )} +
+
+
+
+ {money(t.amount)} +
+ {t.runningBalance != null && ( +
{money(t.runningBalance)}
+ )} +
+
+ ))} +
+ ))} +
+ ); +} diff --git a/frontend/src/pages/Budgets.jsx b/frontend/src/pages/Budgets.jsx index 68ee95d..a008863 100644 --- a/frontend/src/pages/Budgets.jsx +++ b/frontend/src/pages/Budgets.jsx @@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { BudgetsApi, CategoriesApi } from '../api/ledger'; import { localYearMonth } from '../utils/date'; import { useStickyHeader } from '../hooks/useStickyHeader'; +import BudgetTransactionList from '../components/BudgetTransactionList'; function shiftMonth(yearMonth, delta) { const [year, month] = yearMonth.split('-').map(Number); @@ -84,6 +85,7 @@ export default function Budgets() { }; const [confirmingId, setConfirmingId] = useState(null); + const [selectedBudgetId, setSelectedBudgetId] = useState(null); const deleteMutation = useMutation({ mutationFn: BudgetsApi.remove, @@ -91,6 +93,7 @@ export default function Budgets() { queryClient.invalidateQueries({ queryKey: ['budgets', yearMonth] }); queryClient.invalidateQueries({ queryKey: ['budgets-spend', yearMonth] }); setConfirmingId(null); + if (selectedBudgetId) setSelectedBudgetId(null); }, }); @@ -99,6 +102,8 @@ export default function Budgets() { deleteMutation.reset(); }; + const selectedBudget = budgets.find((b) => b.id === selectedBudgetId); + const monthLabel = new Date(`${yearMonth}-01T00:00:00`).toLocaleDateString('en-US', { month: 'long', year: 'numeric', @@ -235,7 +240,14 @@ export default function Budgets() { const color = b.category?.colorHex || categoryColors[b.category?.name] || '#8B92A0'; return (
-
+
{ + if (spent === 0) return; + setSelectedBudgetId(b.id); + }} + > {confirmingId === b.id ? (
Delete budget for “{b.category?.name}”?
@@ -268,13 +280,17 @@ export default function Budgets() { {b.category?.name}
- @@ -294,6 +310,26 @@ export default function Budgets() { ); })}
+ + {selectedBudget && ( +
+
+
{selectedBudget.category?.name} transactions
+ +
+ +
+ )}
); } -- 2.54.0 From cadc5a188a97e8cf9495c5fb41d91020aea9bdc2 Mon Sep 17 00:00:00 2001 From: Ricardo Campos Date: Fri, 31 Jul 2026 20:52:10 +0200 Subject: [PATCH 4/4] fix: add missing cancel button when editing budgets --- CHANGELOG.md | 3 +++ frontend/src/pages/Budgets.jsx | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 185f10f..6ce993a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Category and input filter in the transactions page now show the sum of all matching transactions. +### Fixed +- Missing cancel button when editing a budget value. + --- ## 2026-07-14 diff --git a/frontend/src/pages/Budgets.jsx b/frontend/src/pages/Budgets.jsx index a008863..a750896 100644 --- a/frontend/src/pages/Budgets.jsx +++ b/frontend/src/pages/Budgets.jsx @@ -185,6 +185,19 @@ export default function Budgets() { {upsertMutation.isPending ? 'Saving…' : 'Save'}
+
+ +
-- 2.54.0