feat(expenses): enhance expenses management with filtering and search capabilities

This commit is contained in:
Jose Selesan
2026-05-29 14:56:35 -03:00
parent e99e97aa14
commit d3cf386be5
9 changed files with 379 additions and 17 deletions

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
import { createContext, useContext, useState, useCallback, useEffect, useRef, type ReactNode } from "react";
import {
usePeriodicExpenses,
useCreatePeriodicExpense,
@@ -37,6 +37,10 @@ interface ExpensesContextValue {
page: number;
setPage: (page: number) => void;
pageSize: number;
periodicExpenseFilter: number | undefined;
setPeriodicExpenseFilter: (id: number | undefined) => void;
searchInput: string;
setSearchInput: (value: string) => void;
monthlyTotals: MonthlyTotals | undefined;
isLoadingTotals: boolean;
@@ -59,10 +63,29 @@ export function ExpensesProvider({ children }: { children: ReactNode }) {
const [statusFilter, setStatusFilter] = useState("PENDING");
const [page, setPage] = useState(1);
const pageSize = 10;
const [periodicExpenseFilter, setPeriodicExpenseFilter] = useState<number | undefined>(undefined);
const [searchInput, setSearchInput] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const searchTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
useEffect(() => {
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
searchTimerRef.current = setTimeout(() => {
setSearchQuery(searchInput);
}, 300);
return () => {
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
};
}, [searchInput]);
useEffect(() => {
setPage(1);
}, [periodicExpenseFilter, searchQuery]);
const { data: periodicExpenses = [], isLoading: isLoadingPeriodic } = usePeriodicExpenses();
const { data: expenses = { data: [], total: 0, page: 1, pageSize: 10 }, isLoading: isLoadingExpenses } =
useExpenses(statusFilter, page, pageSize);
useExpenses(statusFilter, page, pageSize, periodicExpenseFilter, searchQuery || undefined);
const now = new Date();
const { data: monthlyTotals, isLoading: isLoadingTotals } = useMonthlyTotals(now.getFullYear(), now.getMonth() + 1);
@@ -145,6 +168,10 @@ export function ExpensesProvider({ children }: { children: ReactNode }) {
page,
setPage,
pageSize,
periodicExpenseFilter,
setPeriodicExpenseFilter,
searchInput,
setSearchInput,
monthlyTotals,
isLoadingTotals,
totalPending,

View File

@@ -11,7 +11,15 @@ import {
ResponsiveDialogDescription,
} from "@/components/ui/responsive-dialog";
import { Button } from "@/components/ui/button";
import { Plus, Upload } from "lucide-react";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Plus, Upload, Search, X } from "lucide-react";
import type { Expense, ImportExpensesResult } from "@/lib/api";
const priceFormatter = new Intl.NumberFormat("es-AR", {
@@ -32,6 +40,11 @@ export function ExpensesTabContent() {
isLoadingTotals,
totalPending,
isLoadingTotalPending,
periodicExpenses,
periodicExpenseFilter,
setPeriodicExpenseFilter,
searchInput,
setSearchInput,
importExpenses,
} = useExpensesContext();
@@ -64,7 +77,44 @@ export function ExpensesTabContent() {
</Button>
))}
</div>
<div className="flex gap-2">
<div className="flex items-center gap-2">
<div className="relative w-48">
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Buscar por descripción..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className="pl-8 pr-8"
/>
{searchInput && (
<button
type="button"
onClick={() => setSearchInput("")}
className="absolute right-1.5 top-1/2 -translate-y-1/2 flex size-5 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
>
<X className="size-3.5" />
</button>
)}
</div>
<Select
value={periodicExpenseFilter !== undefined ? String(periodicExpenseFilter) : "all"}
onValueChange={(val) => {
setPeriodicExpenseFilter(val === "all" ? undefined : Number(val));
setPage(1);
}}
>
<SelectTrigger className="w-52">
<SelectValue placeholder="Todos los gastos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los gastos periódicos</SelectItem>
{periodicExpenses.map((pe) => (
<SelectItem key={pe.id} value={String(pe.id)}>
{pe.description}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => setImportDialogOpen(true)} variant="outline" size="sm">
<Upload className="size-4" />
Importar