Compare commits

...

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
7edcfd334c refactor(frontend): Address code review feedback on focus and currency handling.
Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
2025-12-07 01:33:45 +00:00
copilot-swe-agent[bot]
b1ce242da1 feat(frontend): Fix search focus issue and add transaction statistics.
Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
2025-12-07 01:32:03 +00:00
copilot-swe-agent[bot]
b1364cd605 Initial plan 2025-12-07 01:21:44 +00:00
2 changed files with 125 additions and 3 deletions

View File

@@ -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<ApiResponse<Transaction[]>>({
} = useQuery<PaginatedResponse<Transaction>>({
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,28 @@ 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)
);
// Get currency from first transaction, fallback to EUR
const displayCurrency = transactions.length > 0 ? transactions[0].transaction_currency : "EUR";
const stats = {
totalCount: pagination?.total || 0,
pageCount: transactions.length,
totalIncome,
totalExpenses,
netChange: totalIncome - totalExpenses,
};
return (
<div className="space-y-6 max-w-full">
{/* New FilterBar */}
@@ -350,6 +379,78 @@ export default function TransactionsTable() {
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, displayCurrency)}
</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, displayCurrency)}
</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, displayCurrency)}
</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 */}
<Card>
{/* Desktop Table View (hidden on mobile) */}

View File

@@ -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,24 @@ export function FilterBar({
isSearchLoading = false,
className,
}: FilterBarProps) {
const searchInputRef = useRef<HTMLInputElement>(null);
// Maintain focus on search input during re-renders
useEffect(() => {
const currentInput = searchInputRef.current;
if (!currentInput) return;
// Only restore focus if the search input had focus before
const wasFocused = document.activeElement === currentInput;
// Use requestAnimationFrame to restore focus after React finishes rendering
if (wasFocused && document.activeElement !== currentInput) {
requestAnimationFrame(() => {
currentInput.focus();
});
}
}, [isSearchLoading]);
const hasActiveFilters =
filterState.searchTerm ||
filterState.selectedAccount ||
@@ -61,6 +80,7 @@ export function FilterBar({
<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" />
<Input
ref={searchInputRef}
placeholder="Search transactions..."
value={filterState.searchTerm}
onChange={(e) => onFilterChange("searchTerm", e.target.value)}
@@ -99,6 +119,7 @@ export function FilterBar({
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
ref={searchInputRef}
placeholder="Search..."
value={filterState.searchTerm}
onChange={(e) => onFilterChange("searchTerm", e.target.value)}