From 402f0072af04ac7b301462c2c8f39735a5965e6d Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Sun, 31 May 2026 15:18:23 -0300 Subject: [PATCH] feat(lancamentos): melhora painel de filtros ativos --- .../components/table/transactions-filters.tsx | 901 +++++++++++------- 1 file changed, 554 insertions(+), 347 deletions(-) diff --git a/src/features/transactions/components/table/transactions-filters.tsx b/src/features/transactions/components/table/transactions-filters.tsx index 5756015..923d58f 100644 --- a/src/features/transactions/components/table/transactions-filters.tsx +++ b/src/features/transactions/components/table/transactions-filters.tsx @@ -34,6 +34,7 @@ import { parseDateFilterParam, parsePositiveAmount, } from "@/features/transactions/lib/page-helpers"; +import { Badge } from "@/shared/components/ui/badge"; import { Button } from "@/shared/components/ui/button"; import { Checkbox } from "@/shared/components/ui/checkbox"; import { @@ -54,6 +55,11 @@ import { DrawerTitle, DrawerTrigger, } from "@/shared/components/ui/drawer"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/shared/components/ui/hover-card"; import { Input } from "@/shared/components/ui/input"; import { Popover, @@ -73,6 +79,8 @@ import { ToggleGroup, ToggleGroupItem, } from "@/shared/components/ui/toggle-group"; +import { formatCurrency } from "@/shared/utils/currency"; +import { formatDateOnly } from "@/shared/utils/date"; import { slugify } from "@/shared/utils/string"; import { cn } from "@/shared/utils/ui"; import { @@ -90,6 +98,36 @@ import type { const FILTER_EMPTY_VALUE = "__all"; +type ActiveFilterChipProps = { + label: string; + onRemove: () => void; + disabled?: boolean; +}; + +function ActiveFilterChip({ + label, + onRemove, + disabled, +}: ActiveFilterChipProps) { + return ( + + {label} + + + ); +} + const normalizeAmountParam = (raw: string): string | null => { const parsed = parsePositiveAmount(raw.trim()); return parsed === null ? null : parsed.toString(); @@ -601,6 +639,140 @@ export function TransactionsFilters({ }); }; + const handleRemoveParams = (keys: string[]) => { + const nextParams = new URLSearchParams(searchParams.toString()); + for (const key of keys) { + nextParams.delete(key); + } + nextParams.delete("page"); + + if (keys.includes(AMOUNT_MIN_PARAM)) { + setValorMinValue(""); + } + if (keys.includes(AMOUNT_MAX_PARAM)) { + setValorMaxValue(""); + } + + startTransition(() => { + const target = nextParams.toString() + ? `${pathname}?${nextParams.toString()}` + : pathname; + router.replace(target, { scroll: false }); + }); + }; + + const handleRemoveMultiValue = (key: string, value: string) => { + handleMultiFilterChange( + key, + getParamValues(key).filter((currentValue) => currentValue !== value), + ); + }; + + const activeFilterChips: Array<{ + key: string; + label: string; + onRemove: () => void; + }> = []; + + const typeValue = searchParams.get("type"); + if (typeValue) { + const label = + TRANSACTION_TYPES.find((value) => slugify(value) === typeValue) ?? + typeValue; + activeFilterChips.push({ + key: `type-${typeValue}`, + label: `Tipo: ${label}`, + onRemove: () => handleRemoveParams(["type"]), + }); + } + + const addMultiValueChips = ( + param: string, + prefix: string, + options: MultiOption[], + ) => { + const labels = new Map( + options.map((option) => [option.value, option.label]), + ); + for (const value of getParamValues(param)) { + activeFilterChips.push({ + key: `${param}-${value}`, + label: `${prefix}: ${labels.get(value) ?? value}`, + onRemove: () => handleRemoveMultiValue(param, value), + }); + } + }; + + addMultiValueChips("condition", "Condição", conditionOptions); + addMultiValueChips("payment", "Pagamento", paymentOptions); + addMultiValueChips("payer", "Pessoa", payerMultiOptions); + addMultiValueChips("category", "Categoria", categoryMultiOptions); + addMultiValueChips("accountCard", "Conta/cartão", accountCardMultiOptions); + + const settledValue = searchParams.get("settled"); + if (settledValue) { + activeFilterChips.push({ + key: `settled-${settledValue}`, + label: + settledValue === SETTLED_FILTER_VALUES.PAID + ? "Status: Pago" + : "Status: Não pago", + onRemove: () => handleRemoveParams(["settled"]), + }); + } + + if (searchParams.get("hasAttachment") === "true") { + activeFilterChips.push({ + key: "has-attachment", + label: "Com anexo", + onRemove: () => handleRemoveParams(["hasAttachment"]), + }); + } + + if (searchParams.get("isDivided") === "true") { + activeFilterChips.push({ + key: "is-divided", + label: "Somente divididos", + onRemove: () => handleRemoveParams(["isDivided"]), + }); + } + + if (hasAmountFilter) { + const minValue = parsePositiveAmount( + searchParams.get(AMOUNT_MIN_PARAM) ?? "", + ); + const maxValue = parsePositiveAmount( + searchParams.get(AMOUNT_MAX_PARAM) ?? "", + ); + const label = + minValue !== null && maxValue !== null + ? `Valor: ${formatCurrency(minValue)} até ${formatCurrency(maxValue)}` + : minValue !== null + ? `Valor: a partir de ${formatCurrency(minValue)}` + : `Valor: até ${formatCurrency(maxValue ?? 0)}`; + activeFilterChips.push({ + key: "amount-range", + label, + onRemove: () => handleRemoveParams([AMOUNT_MIN_PARAM, AMOUNT_MAX_PARAM]), + }); + } + + if (hasDateRangeFilter) { + const startValue = formatDateOnly(searchParams.get(DATE_START_PARAM)); + const endValue = formatDateOnly(searchParams.get(DATE_END_PARAM)); + const label = + startValue && endValue + ? `Datas: ${startValue} até ${endValue}` + : startValue + ? `Datas: a partir de ${startValue}` + : `Datas: até ${endValue}`; + activeFilterChips.push({ + key: "date-range", + label, + onRemove: () => handleRemoveParams([DATE_START_PARAM, DATE_END_PARAM]), + }); + } + return (
- - - - {hasActiveFilters && ( - - )} - - - Filtros - - Selecione os filtros desejados para refinar os lançamentos - - - -
-
-
-
- - ({ - value: slugify(v), - label: v, - }))} - widthClass="w-full border-dashed" - disabled={isPending} - getParamValue={getParamValue} - onChange={handleFilterChange} - renderContent={(label) => ( - - )} - /> -
- -
- - - handleMultiFilterChange("condition", values) - } - disabled={isPending} - /> -
- -
- - - handleMultiFilterChange("payment", values) - } - disabled={isPending} - /> -
- -
- - - handleMultiFilterChange("payer", values) - } - disabled={isPending} - searchable - searchPlaceholder="Buscar pessoa..." - /> -
- -
- - - handleMultiFilterChange("category", values) - } - disabled={isPending} - searchable - searchPlaceholder="Buscar categoria..." - groupOrder={["Despesas", "Receitas", "Outras"]} - /> -
- -
- - - handleMultiFilterChange("accountCard", values) - } - disabled={isPending} - searchable - searchPlaceholder="Buscar conta ou cartão..." - groupOrder={["Contas", "Cartões"]} - /> -
-
-
- - - -
-
-
- - {hasDateRangeFilter ? ( - - ) : null} -
-
- - handleDateFilterChange(DATE_START_PARAM, value) - } - placeholder="Data inicial" - disabled={isPending} - inputClassName="border-dashed" - compact - /> - - até - - - handleDateFilterChange(DATE_END_PARAM, value) - } - placeholder="Data final" - disabled={isPending} - inputClassName="border-dashed" - compact - /> -
-
- -
- -
- - setValorMinValue(event.target.value) - } - disabled={isPending} - className="text-sm border-dashed" - /> - até - - setValorMaxValue(event.target.value) - } - disabled={isPending} - className="text-sm border-dashed" - /> -
-
-
- - - -
- { - if (!value) return; - handleFilterChange( - "settled", - value === FILTER_EMPTY_VALUE ? null : value, - ); - }} + + + + +
- -
- - { - handleFilterChange( - "hasAttachment", - checked ? "true" : null, - ); - }} - /> -
- -
- - { - handleFilterChange("isDivided", checked ? "true" : null); - }} - /> -
-
- - -
-
- - {hasActiveFilters - ? `${activeFilterCount} ${ - activeFilterCount === 1 - ? "filtro ativo" - : "filtros ativos" - }` - : "Nenhum filtro ativo"} - {isPending ? ( + + ) : ( + + )} + {isPending ? "Aplicando..." : "Filtros"} + {hasActiveFilters && ( - - Aplicando filtros... - - ) : null} + className="absolute -top-1 -right-1 size-3 rounded-full bg-primary" + aria-hidden + /> + )} + + + + {activeFilterChips.length > 0 ? ( + +
+

Filtros ativos

+

+ Remova rapidamente o que não precisa mais. +

+
+
+ {activeFilterChips.map((chip) => ( + + ))}
+
+ ) : null} + + + Filtros + + Selecione os filtros desejados para refinar os lançamentos + + + +
+
+
+
+ + ({ + value: slugify(v), + label: v, + }))} + widthClass="w-full border-dashed" + disabled={isPending} + getParamValue={getParamValue} + onChange={handleFilterChange} + renderContent={(label) => ( + + )} + /> +
+ +
+ + + handleMultiFilterChange("condition", values) + } + disabled={isPending} + /> +
+ +
+ + + handleMultiFilterChange("payment", values) + } + disabled={isPending} + /> +
+ +
+ + + handleMultiFilterChange("payer", values) + } + disabled={isPending} + searchable + searchPlaceholder="Buscar pessoa..." + /> +
+ +
+ + + handleMultiFilterChange("category", values) + } + disabled={isPending} + searchable + searchPlaceholder="Buscar categoria..." + groupOrder={["Despesas", "Receitas", "Outras"]} + /> +
+ +
+ + + handleMultiFilterChange("accountCard", values) + } + disabled={isPending} + searchable + searchPlaceholder="Buscar conta ou cartão..." + groupOrder={["Contas", "Cartões"]} + /> +
+
+
+ + + +
+
+
+ + {hasDateRangeFilter ? ( + + ) : null} +
+
+ + handleDateFilterChange(DATE_START_PARAM, value) + } + placeholder="Data inicial" + disabled={isPending} + inputClassName="border-dashed" + compact + /> + + até + + + handleDateFilterChange(DATE_END_PARAM, value) + } + placeholder="Data final" + disabled={isPending} + inputClassName="border-dashed" + compact + /> +
+
+ +
+ +
+ + setValorMinValue(event.target.value) + } + disabled={isPending} + className="text-sm border-dashed" + /> + + até + + + setValorMaxValue(event.target.value) + } + disabled={isPending} + className="text-sm border-dashed" + /> +
+
+
+ + + +
+ { + if (!value) return; + handleFilterChange( + "settled", + value === FILTER_EMPTY_VALUE ? null : value, + ); + }} + variant="outline" + size="sm" + className="grid w-full grid-cols-3 rounded-md bg-muted/30 p-0.5" + aria-label="Status de pagamento" + > + + Todos + + + Pagos + + + Não pagos + + +
+ +
+ + { + handleFilterChange( + "hasAttachment", + checked ? "true" : null, + ); + }} + /> +
+ +
+ + { + handleFilterChange( + "isDivided", + checked ? "true" : null, + ); + }} + /> +
- -
- + + +
+
+ + {hasActiveFilters + ? `${activeFilterCount} ${ + activeFilterCount === 1 + ? "filtro ativo" + : "filtros ativos" + }` + : "Nenhum filtro ativo"} + + {isPending ? ( + + + Aplicando filtros... + + ) : null} +
+ +
+
+ + + )}