mirror of
https://github.com/elisiariocouto/leggen.git
synced 2026-01-30 17:28:29 +00:00
feat(frontend): Fix search focus issue and add transaction statistics.
Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
This commit is contained in:
committed by
Elisiário Couto
parent
88037f328d
commit
2c85722fd0
@@ -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 (
|
return (
|
||||||
<div className="space-y-6 max-w-full">
|
<div className="space-y-6 max-w-full">
|
||||||
{/* New FilterBar */}
|
{/* New FilterBar */}
|
||||||
@@ -366,6 +385,78 @@ export default function TransactionsTable() {
|
|||||||
isSearchLoading={isSearchLoading}
|
isSearchLoading={isSearchLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Transaction Statistics */}
|
||||||
|
{transactions.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||||
|
Showing
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-foreground mt-1">
|
||||||
|
{stats.pageCount}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
of {stats.totalCount} total
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||||
|
Income
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-green-600 mt-1">
|
||||||
|
+{formatCurrency(stats.totalIncome, transactions[0]?.transaction_currency || "EUR")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TrendingUp className="h-8 w-8 text-green-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||||
|
Expenses
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold text-red-600 mt-1">
|
||||||
|
-{formatCurrency(stats.totalExpenses, transactions[0]?.transaction_currency || "EUR")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TrendingDown className="h-8 w-8 text-red-600 opacity-50" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||||
|
Net Change
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={`text-2xl font-bold mt-1 ${
|
||||||
|
stats.netChange >= 0 ? "text-green-600" : "text-red-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{stats.netChange >= 0 ? "+" : ""}
|
||||||
|
{formatCurrency(stats.netChange, transactions[0]?.transaction_currency || "EUR")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{stats.netChange >= 0 ? (
|
||||||
|
<TrendingUp className="h-8 w-8 text-green-600 opacity-50" />
|
||||||
|
) : (
|
||||||
|
<TrendingDown className="h-8 w-8 text-red-600 opacity-50" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Responsive Table/Cards */}
|
{/* Responsive Table/Cards */}
|
||||||
<Card>
|
<Card>
|
||||||
{/* Desktop Table View (hidden on mobile) */}
|
{/* Desktop Table View (hidden on mobile) */}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRef, useEffect } from "react";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -30,6 +31,26 @@ export function FilterBar({
|
|||||||
isSearchLoading = false,
|
isSearchLoading = false,
|
||||||
className,
|
className,
|
||||||
}: FilterBarProps) {
|
}: FilterBarProps) {
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(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 =
|
const hasActiveFilters =
|
||||||
filterState.searchTerm ||
|
filterState.searchTerm ||
|
||||||
filterState.selectedAccount ||
|
filterState.selectedAccount ||
|
||||||
@@ -61,6 +82,7 @@ export function FilterBar({
|
|||||||
<div className="relative w-[200px]">
|
<div className="relative w-[200px]">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
placeholder="Search transactions..."
|
placeholder="Search transactions..."
|
||||||
value={filterState.searchTerm}
|
value={filterState.searchTerm}
|
||||||
onChange={(e) => onFilterChange("searchTerm", e.target.value)}
|
onChange={(e) => onFilterChange("searchTerm", e.target.value)}
|
||||||
@@ -99,6 +121,7 @@ export function FilterBar({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
value={filterState.searchTerm}
|
value={filterState.searchTerm}
|
||||||
onChange={(e) => onFilterChange("searchTerm", e.target.value)}
|
onChange={(e) => onFilterChange("searchTerm", e.target.value)}
|
||||||
|
|||||||
Reference in New Issue
Block a user