feat(expenses): enhance expenses management with filtering and search capabilities
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user