From b1ce242da17b2eb0f2941373dda19093343797ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:32:03 +0000 Subject: [PATCH] feat(frontend): Fix search focus issue and add transaction statistics. Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com> --- frontend/src/components/TransactionsTable.tsx | 104 +++++++++++++++++- frontend/src/components/filters/FilterBar.tsx | 23 ++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/TransactionsTable.tsx b/frontend/src/components/TransactionsTable.tsx index 1feb559..555b6b0 100644 --- a/frontend/src/components/TransactionsTable.tsx +++ b/frontend/src/components/TransactionsTable.tsx @@ -31,7 +31,7 @@ import { DataTablePagination } from "./ui/data-table-pagination"; import { Card } from "./ui/card"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Button } from "./ui/button"; -import type { Account, Transaction, ApiResponse } from "../types/api"; +import type { Account, Transaction, PaginatedResponse } from "../types/api"; export default function TransactionsTable() { // Filter state consolidated into a single object @@ -102,7 +102,7 @@ export default function TransactionsTable() { isLoading: transactionsLoading, error: transactionsError, refetch: refetchTransactions, - } = useQuery>({ + } = useQuery>({ queryKey: [ "transactions", filterState.selectedAccount, @@ -125,7 +125,14 @@ export default function TransactionsTable() { }); const transactions = transactionsResponse?.data || []; - const pagination = transactionsResponse?.pagination; + const pagination = transactionsResponse ? { + page: transactionsResponse.page, + per_page: transactionsResponse.per_page, + total: transactionsResponse.total, + total_pages: transactionsResponse.total_pages, + has_next: transactionsResponse.has_next, + has_prev: transactionsResponse.has_prev, + } : undefined; // Check if search is currently debouncing const isSearchLoading = filterState.searchTerm !== debouncedSearchTerm; @@ -339,6 +346,25 @@ export default function TransactionsTable() { ); } + // Calculate stats from current page transactions + const totalIncome = transactions + .filter((t: Transaction) => t.transaction_value > 0) + .reduce((sum: number, t: Transaction) => sum + t.transaction_value, 0); + + const totalExpenses = Math.abs( + transactions + .filter((t: Transaction) => t.transaction_value < 0) + .reduce((sum: number, t: Transaction) => sum + t.transaction_value, 0) + ); + + const stats = { + totalCount: pagination?.total || 0, + pageCount: transactions.length, + totalIncome, + totalExpenses, + netChange: totalIncome - totalExpenses, + }; + return (
{/* New FilterBar */} @@ -350,6 +376,78 @@ export default function TransactionsTable() { isSearchLoading={isSearchLoading} /> + {/* Transaction Statistics */} + {transactions.length > 0 && ( +
+ +
+
+

+ Showing +

+

+ {stats.pageCount} +

+

+ of {stats.totalCount} total +

+
+
+
+ + +
+
+

+ Income +

+

+ +{formatCurrency(stats.totalIncome, transactions[0]?.transaction_currency || "EUR")} +

+
+ +
+
+ + +
+
+

+ Expenses +

+

+ -{formatCurrency(stats.totalExpenses, transactions[0]?.transaction_currency || "EUR")} +

+
+ +
+
+ + +
+
+

+ Net Change +

+

= 0 ? "text-green-600" : "text-red-600" + }`} + > + {stats.netChange >= 0 ? "+" : ""} + {formatCurrency(stats.netChange, transactions[0]?.transaction_currency || "EUR")} +

+
+ {stats.netChange >= 0 ? ( + + ) : ( + + )} +
+
+
+ )} + {/* Responsive Table/Cards */} {/* Desktop Table View (hidden on mobile) */} diff --git a/frontend/src/components/filters/FilterBar.tsx b/frontend/src/components/filters/FilterBar.tsx index 8400680..2b60ab4 100644 --- a/frontend/src/components/filters/FilterBar.tsx +++ b/frontend/src/components/filters/FilterBar.tsx @@ -1,3 +1,4 @@ +import { useRef, useEffect } from "react"; import { Search } from "lucide-react"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; @@ -30,6 +31,26 @@ export function FilterBar({ isSearchLoading = false, className, }: FilterBarProps) { + const searchInputRef = useRef(null); + const wasFocused = useRef(false); + + // Track if search input was focused before re-render + useEffect(() => { + const currentInput = searchInputRef.current; + if (!currentInput) return; + + // Check if the input was focused before the effect runs + if (document.activeElement === currentInput) { + wasFocused.current = true; + } + + // If it was focused, restore focus after render + if (wasFocused.current && document.activeElement !== currentInput) { + currentInput.focus(); + wasFocused.current = false; + } + }); + const hasActiveFilters = filterState.searchTerm || filterState.selectedAccount || @@ -61,6 +82,7 @@ export function FilterBar({
onFilterChange("searchTerm", e.target.value)} @@ -99,6 +121,7 @@ export function FilterBar({
onFilterChange("searchTerm", e.target.value)}