mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
feat(lancamentos): melhora painel de filtros ativos
This commit is contained in:
@@ -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 (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="gap-1 border border-border/70 bg-secondary/70 py-1 pr-1 pl-2.5 font-normal text-secondary-foreground"
|
||||
>
|
||||
<span>{label}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
disabled={disabled}
|
||||
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-background/80 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||
aria-label={`Remover filtro ${label}`}
|
||||
>
|
||||
<RiCloseLine className="size-3" aria-hidden />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
aria-busy={isPending}
|
||||
@@ -640,19 +812,27 @@ export function TransactionsFilters({
|
||||
)}
|
||||
|
||||
{!hideAdvancedFilters && (
|
||||
<HoverCard openDelay={200} closeDelay={200}>
|
||||
<Drawer
|
||||
direction="right"
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 md:flex-none text-sm border-dashed relative bg-transparent"
|
||||
aria-label={isPending ? "Aplicando filtros" : "Abrir filtros"}
|
||||
aria-label={
|
||||
isPending ? "Aplicando filtros" : "Abrir filtros"
|
||||
}
|
||||
>
|
||||
{isPending ? (
|
||||
<Spinner className="size-4" role="presentation" aria-hidden />
|
||||
<Spinner
|
||||
className="size-4"
|
||||
role="presentation"
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<RiFilterLine className="size-4" aria-hidden />
|
||||
)}
|
||||
@@ -665,20 +845,41 @@ export function TransactionsFilters({
|
||||
)}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
{hasActiveFilters && (
|
||||
</HoverCardTrigger>
|
||||
{activeFilterChips.length > 0 ? (
|
||||
<HoverCardContent
|
||||
align="end"
|
||||
className="w-80 space-y-3"
|
||||
aria-label="Filtros ativos"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Filtros ativos</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Remova rapidamente o que não precisa mais.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{activeFilterChips.map((chip) => (
|
||||
<ActiveFilterChip
|
||||
key={chip.key}
|
||||
label={chip.label}
|
||||
onRemove={chip.onRemove}
|
||||
disabled={isPending}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={isPending}
|
||||
aria-label="Limpar filtros"
|
||||
className="text-xs text-muted-foreground hover:text-foreground h-9 px-2"
|
||||
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<RiCloseLine className="size-3.5" aria-hidden />
|
||||
Limpar
|
||||
Limpar filtros
|
||||
</Button>
|
||||
)}
|
||||
</HoverCardContent>
|
||||
) : null}
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Filtros</DrawerTitle>
|
||||
@@ -861,7 +1062,9 @@ export function TransactionsFilters({
|
||||
disabled={isPending}
|
||||
className="text-sm border-dashed"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">até</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
até
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
@@ -951,7 +1154,10 @@ export function TransactionsFilters({
|
||||
checked={searchParams.get("isDivided") === "true"}
|
||||
disabled={isPending}
|
||||
onCheckedChange={(checked) => {
|
||||
handleFilterChange("isDivided", checked ? "true" : null);
|
||||
handleFilterChange(
|
||||
"isDivided",
|
||||
checked ? "true" : null,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -1000,6 +1206,7 @@ export function TransactionsFilters({
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</HoverCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user