feat: filtro por faixa de valor e botão limpar em lançamentos

Novo filtro mín/máx de valor no sheet de filtros, com debounce (400ms) e
persistência via query string (amountMin/amountMax). Constantes AMOUNT_MIN_PARAM
e AMOUNT_MAX_PARAM extraídas para constants.ts; parsePositiveAmount exportado de
page-helpers e reutilizado pelo useDebouncedAmountFilter. A comparação do
debounce usa o valor normalizado para evitar roundtrips RSC desnecessários.
Botão 'Limpar' discreto ao lado do botão 'Filtros', visível apenas quando
há filtros ativos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-05-10 13:51:30 +00:00
parent 7128cc0ae7
commit c9239c4f3c
6 changed files with 152 additions and 5 deletions

View File

@@ -1,5 +1,15 @@
import type { SQL } from "drizzle-orm";
import { and, eq, ilike, inArray, isNotNull, or, sql } from "drizzle-orm";
import {
and,
eq,
gte,
ilike,
inArray,
isNotNull,
lte,
or,
sql,
} from "drizzle-orm";
import {
cards,
type categories,
@@ -10,6 +20,8 @@ import {
} from "@/db/schema";
import type { SelectOption } from "@/features/transactions/components/types";
import {
AMOUNT_MAX_PARAM,
AMOUNT_MIN_PARAM,
PAYMENT_METHODS,
SETTLED_FILTER_VALUES,
TRANSACTION_CONDITIONS,
@@ -46,6 +58,8 @@ export type TransactionSearchFilters = {
settledFilter: string | null;
attachmentFilter: string | null;
dividedFilter: string | null;
amountMinFilter: number | null;
amountMaxFilter: number | null;
};
type BaseSluggedOption = {
@@ -135,6 +149,13 @@ export const getMultiParam = (
return list.filter((item): item is string => Boolean(item));
};
export const parsePositiveAmount = (value: string | null): number | null => {
if (!value) return null;
const normalized = Number.parseFloat(value.replace(",", "."));
if (!Number.isFinite(normalized) || normalized < 0) return null;
return Math.round(normalized * 100) / 100;
};
export const extractTransactionSearchFilters = (
params: ResolvedSearchParams,
): TransactionSearchFilters => ({
@@ -148,6 +169,12 @@ export const extractTransactionSearchFilters = (
settledFilter: getSingleParam(params, "settled"),
attachmentFilter: getSingleParam(params, "hasAttachment"),
dividedFilter: getSingleParam(params, "isDivided"),
amountMinFilter: parsePositiveAmount(
getSingleParam(params, AMOUNT_MIN_PARAM),
),
amountMaxFilter: parsePositiveAmount(
getSingleParam(params, AMOUNT_MAX_PARAM),
),
});
export const resolveTransactionPagination = (
@@ -442,6 +469,18 @@ export const buildTransactionWhere = ({
where.push(eq(transactions.isDivided, true));
}
if (filters.amountMinFilter !== null) {
where.push(
gte(sql`abs(${transactions.amount})`, filters.amountMinFilter.toFixed(2)),
);
}
if (filters.amountMaxFilter !== null) {
where.push(
lte(sql`abs(${transactions.amount})`, filters.amountMaxFilter.toFixed(2)),
);
}
const searchPattern = buildSearchPattern(filters.searchFilter);
if (searchPattern) {
where.push(