diff --git a/frontend/src/components/TransactionsTable.tsx b/frontend/src/components/TransactionsTable.tsx index 13fb601..bf624b2 100644 --- a/frontend/src/components/TransactionsTable.tsx +++ b/frontend/src/components/TransactionsTable.tsx @@ -355,6 +355,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 */} @@ -366,6 +385,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)}