feat(lancamentos): adiciona filtro por intervalo de datas

This commit is contained in:
Felipe Coutinho
2026-05-23 13:17:42 -03:00
parent b9557961e5
commit 7a0e33efd8
8 changed files with 384 additions and 194 deletions

View File

@@ -87,6 +87,8 @@ const EMPTY_FILTERS: TransactionSearchFilters = {
dividedFilter: null, dividedFilter: null,
amountMinFilter: null, amountMinFilter: null,
amountMaxFilter: null, amountMaxFilter: null,
dateStartFilter: null,
dateEndFilter: null,
}; };
const createEmptySlugMaps = (): SlugMaps => ({ const createEmptySlugMaps = (): SlugMaps => ({

View File

@@ -36,6 +36,14 @@ const exportTransactionsSchema: z.ZodType<TransactionsExportContext> = z.object(
dividedFilter: z.string().nullable(), dividedFilter: z.string().nullable(),
amountMinFilter: z.number().nullable(), amountMinFilter: z.number().nullable(),
amountMaxFilter: z.number().nullable(), amountMaxFilter: z.number().nullable(),
dateStartFilter: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.nullable(),
dateEndFilter: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.nullable(),
}), }),
accountId: z.string().min(1).nullable().optional(), accountId: z.string().min(1).nullable().optional(),
cardId: z.string().min(1).nullable().optional(), cardId: z.string().min(1).nullable().optional(),

View File

@@ -23,12 +23,17 @@ import {
import { import {
AMOUNT_MAX_PARAM, AMOUNT_MAX_PARAM,
AMOUNT_MIN_PARAM, AMOUNT_MIN_PARAM,
DATE_END_PARAM,
DATE_START_PARAM,
PAYMENT_METHODS, PAYMENT_METHODS,
SETTLED_FILTER_VALUES, SETTLED_FILTER_VALUES,
TRANSACTION_CONDITIONS, TRANSACTION_CONDITIONS,
TRANSACTION_TYPES, TRANSACTION_TYPES,
} from "@/features/transactions/lib/constants"; } from "@/features/transactions/lib/constants";
import { parsePositiveAmount } from "@/features/transactions/lib/page-helpers"; import {
parseDateFilterParam,
parsePositiveAmount,
} from "@/features/transactions/lib/page-helpers";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Checkbox } from "@/shared/components/ui/checkbox"; import { Checkbox } from "@/shared/components/ui/checkbox";
import { import {
@@ -39,6 +44,7 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/shared/components/ui/command"; } from "@/shared/components/ui/command";
import { DatePicker } from "@/shared/components/ui/date-picker";
import { import {
Drawer, Drawer,
DrawerContent, DrawerContent,
@@ -60,7 +66,12 @@ import {
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
} from "@/shared/components/ui/select"; } from "@/shared/components/ui/select";
import { Separator } from "@/shared/components/ui/separator";
import { Switch } from "@/shared/components/ui/switch"; import { Switch } from "@/shared/components/ui/switch";
import {
ToggleGroup,
ToggleGroupItem,
} from "@/shared/components/ui/toggle-group";
import { slugify } from "@/shared/utils/string"; import { slugify } from "@/shared/utils/string";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
import { import {
@@ -83,6 +94,9 @@ const normalizeAmountParam = (raw: string): string | null => {
return parsed === null ? null : parsed.toString(); return parsed === null ? null : parsed.toString();
}; };
const normalizeDateParam = (raw: string): string | null =>
parseDateFilterParam(raw.trim());
function useDebouncedAmountFilter( function useDebouncedAmountFilter(
param: string, param: string,
searchParams: URLSearchParams | ReadonlyURLSearchParams, searchParams: URLSearchParams | ReadonlyURLSearchParams,
@@ -135,6 +149,7 @@ function FilterSelect({
value === FILTER_EMPTY_VALUE value === FILTER_EMPTY_VALUE
? placeholder ? placeholder
: (current?.label ?? placeholder); : (current?.label ?? placeholder);
const hasSelection = value !== FILTER_EMPTY_VALUE && Boolean(current);
return ( return (
<Select <Select
@@ -148,8 +163,13 @@ function FilterSelect({
className={cn("text-sm border-dashed", widthClass)} className={cn("text-sm border-dashed", widthClass)}
disabled={disabled} disabled={disabled}
> >
<span className="truncate"> <span
{value !== FILTER_EMPTY_VALUE && current && renderContent className={cn(
"truncate",
hasSelection ? "text-foreground" : "text-muted-foreground",
)}
>
{hasSelection && renderContent
? renderContent(current.label) ? renderContent(current.label)
: displayLabel} : displayLabel}
</span> </span>
@@ -255,12 +275,19 @@ function MultiSelectFilter({
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
"justify-between text-sm border-dashed font-normal", "justify-between text-sm border-dashed font-normal shadow-none",
widthClass, widthClass,
)} )}
disabled={disabled} disabled={disabled}
> >
<span className="truncate flex items-center gap-2"> <span
className={cn(
"truncate flex items-center gap-2",
selectedOptions.length > 0
? "text-foreground"
: "text-muted-foreground",
)}
>
{triggerLabel} {triggerLabel}
</span> </span>
<RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" /> <RiExpandUpDownLine className="ml-2 size-4 shrink-0 opacity-50" />
@@ -392,6 +419,13 @@ export function TransactionsFilters({
[searchParams, pathname, router], [searchParams, pathname, router],
); );
const handleDateFilterChange = useCallback(
(key: string, value: string) => {
handleFilterChange(key, normalizeDateParam(value));
},
[handleFilterChange],
);
const [searchValue, setSearchValue] = useState(searchParams.get("q") ?? ""); const [searchValue, setSearchValue] = useState(searchParams.get("q") ?? "");
const currentSearchParam = searchParams.get("q") ?? ""; const currentSearchParam = searchParams.get("q") ?? "";
@@ -509,25 +543,46 @@ export function TransactionsFilters({
); );
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const hasDateRangeFilter =
const hasActiveFilters = Boolean(searchParams.get(DATE_START_PARAM)) ||
searchParams.get("type") || Boolean(searchParams.get(DATE_END_PARAM));
searchParams.getAll("condition").length > 0 || const hasAmountFilter =
searchParams.getAll("payment").length > 0 || Boolean(searchParams.get(AMOUNT_MIN_PARAM)) ||
searchParams.getAll("payer").length > 0 || Boolean(searchParams.get(AMOUNT_MAX_PARAM));
searchParams.getAll("category").length > 0 || const activeFilterCount = [
searchParams.getAll("accountCard").length > 0 || Boolean(searchParams.get("type")),
searchParams.get("settled") || searchParams.getAll("condition").length > 0,
searchParams.get("hasAttachment") || searchParams.getAll("payment").length > 0,
searchParams.get("isDivided") || searchParams.getAll("payer").length > 0,
searchParams.get(AMOUNT_MIN_PARAM) || searchParams.getAll("category").length > 0,
searchParams.get(AMOUNT_MAX_PARAM); searchParams.getAll("accountCard").length > 0,
Boolean(searchParams.get("settled")),
Boolean(searchParams.get("hasAttachment")),
Boolean(searchParams.get("isDivided")),
hasAmountFilter,
hasDateRangeFilter,
].filter(Boolean).length;
const hasActiveFilters = activeFilterCount > 0;
const settledFilterValue = searchParams.get("settled") ?? FILTER_EMPTY_VALUE;
const handleResetFilters = () => { const handleResetFilters = () => {
handleReset(); handleReset();
setDrawerOpen(false); setDrawerOpen(false);
}; };
const handleResetDateRange = () => {
const nextParams = new URLSearchParams(searchParams.toString());
nextParams.delete(DATE_START_PARAM);
nextParams.delete(DATE_END_PARAM);
nextParams.delete("page");
startTransition(() => {
const target = nextParams.toString()
? `${pathname}?${nextParams.toString()}`
: pathname;
router.replace(target, { scroll: false });
});
};
return ( return (
<div <div
className={cn( className={cn(
@@ -607,182 +662,234 @@ export function TransactionsFilters({
</DrawerHeader> </DrawerHeader>
<div className="flex-1 overflow-y-auto px-4 space-y-4"> <div className="flex-1 overflow-y-auto px-4 space-y-4">
<div className="space-y-2"> <div>
<label className="text-sm font-medium"> <div className="grid gap-3 sm:grid-cols-2">
Tipo de Lançamento <div className="space-y-1.5">
</label> <label className="text-xs font-medium text-muted-foreground">
<FilterSelect Tipo de lançamento
param="type" </label>
placeholder="Todos" <FilterSelect
options={TRANSACTION_TYPES.map((v) => ({ param="type"
value: slugify(v), placeholder="Todos"
label: v, options={TRANSACTION_TYPES.map((v) => ({
}))} value: slugify(v),
widthClass="w-full border-dashed" label: v,
disabled={isPending} }))}
getParamValue={getParamValue} widthClass="w-full border-dashed"
onChange={handleFilterChange} disabled={isPending}
renderContent={(label) => ( getParamValue={getParamValue}
<TransactionTypeSelectContent label={label} /> onChange={handleFilterChange}
)} renderContent={(label) => (
/> <TransactionTypeSelectContent label={label} />
</div> )}
/>
</div>
<div className="space-y-2"> <div className="space-y-1.5">
<label className="text-sm font-medium"> <label className="text-xs font-medium text-muted-foreground">
Condição de Lançamento Condição de pagamento
</label> </label>
<MultiSelectFilter <MultiSelectFilter
placeholder="Todas" placeholder="Todas"
options={conditionOptions} options={conditionOptions}
selected={getParamValues("condition")} selected={getParamValues("condition")}
onChange={(values) => onChange={(values) =>
handleMultiFilterChange("condition", values) handleMultiFilterChange("condition", values)
} }
disabled={isPending} disabled={isPending}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-1.5">
<label className="text-sm font-medium"> <label className="text-xs font-medium text-muted-foreground">
Forma de Pagamento Forma de pagamento
</label> </label>
<MultiSelectFilter <MultiSelectFilter
placeholder="Todas" placeholder="Todas"
options={paymentOptions} options={paymentOptions}
selected={getParamValues("payment")} selected={getParamValues("payment")}
onChange={(values) => onChange={(values) =>
handleMultiFilterChange("payment", values) handleMultiFilterChange("payment", values)
} }
disabled={isPending} disabled={isPending}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-1.5">
<label className="text-sm font-medium">Pessoa</label> <label className="text-xs font-medium text-muted-foreground">
<MultiSelectFilter Pessoa
placeholder="Todas" </label>
options={payerMultiOptions} <MultiSelectFilter
selected={getParamValues("payer")} placeholder="Todas"
onChange={(values) => options={payerMultiOptions}
handleMultiFilterChange("payer", values) selected={getParamValues("payer")}
} onChange={(values) =>
disabled={isPending} handleMultiFilterChange("payer", values)
searchable }
searchPlaceholder="Buscar pessoa..." disabled={isPending}
/> searchable
</div> searchPlaceholder="Buscar pessoa..."
/>
</div>
<div className="space-y-2"> <div className="space-y-1.5">
<label className="text-sm font-medium">Categoria</label> <label className="text-xs font-medium text-muted-foreground">
<MultiSelectFilter Categoria
placeholder="Todas" </label>
options={categoryMultiOptions} <MultiSelectFilter
selected={getParamValues("category")} placeholder="Todas"
onChange={(values) => options={categoryMultiOptions}
handleMultiFilterChange("category", values) selected={getParamValues("category")}
} onChange={(values) =>
disabled={isPending} handleMultiFilterChange("category", values)
searchable }
searchPlaceholder="Buscar categoria..." disabled={isPending}
/> searchable
</div> searchPlaceholder="Buscar categoria..."
/>
</div>
<div className="space-y-2"> <div className="space-y-1.5">
<label className="text-sm font-medium">Conta/Cartão</label> <label className="text-xs font-medium text-muted-foreground">
<MultiSelectFilter Conta/Cartão
placeholder="Todos" </label>
options={accountCardMultiOptions} <MultiSelectFilter
selected={getParamValues("accountCard")} placeholder="Todos"
onChange={(values) => options={accountCardMultiOptions}
handleMultiFilterChange("accountCard", values) selected={getParamValues("accountCard")}
} onChange={(values) =>
disabled={isPending} handleMultiFilterChange("accountCard", values)
searchable }
searchPlaceholder="Buscar conta ou cartão..." disabled={isPending}
groupOrder={["Contas", "Cartões"]} searchable
/> searchPlaceholder="Buscar conta ou cartão..."
</div> groupOrder={["Contas", "Cartões"]}
/>
<div className="space-y-2"> </div>
<label className="text-sm font-medium">Faixa de valor</label>
<div className="flex items-center gap-2">
<Input
type="number"
inputMode="decimal"
min="0"
step="0.01"
placeholder="Mínimo"
aria-label="Valor mínimo"
value={valorMinValue}
onChange={(event) => setValorMinValue(event.target.value)}
disabled={isPending}
className="text-sm border-dashed"
/>
<span className="text-xs text-muted-foreground">até</span>
<Input
type="number"
inputMode="decimal"
min="0"
step="0.01"
placeholder="Máximo"
aria-label="Valor máximo"
value={valorMaxValue}
onChange={(event) => setValorMaxValue(event.target.value)}
disabled={isPending}
className="text-sm border-dashed"
/>
</div> </div>
</div> </div>
<Separator />
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm font-medium">Status</p> <div className="space-y-2">
<div className="space-y-3"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center justify-between"> <label className="text-xs font-medium text-muted-foreground">
<label Período
htmlFor="filter-pago"
className="text-sm text-muted-foreground cursor-pointer"
>
Somente pagos
</label> </label>
<Switch {hasDateRangeFilter ? (
id="filter-pago" <button
checked={ type="button"
searchParams.get("settled") === onClick={handleResetDateRange}
SETTLED_FILTER_VALUES.PAID disabled={isPending}
} className="text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline disabled:pointer-events-none disabled:opacity-50"
disabled={isPending} >
onCheckedChange={(checked) => { Limpar período
handleFilterChange( </button>
"settled", ) : null}
checked ? SETTLED_FILTER_VALUES.PAID : null,
);
}}
/>
</div> </div>
<div className="flex items-center justify-between"> <div className="grid gap-2 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
<label <DatePicker
htmlFor="filter-nao-pago" value={searchParams.get(DATE_START_PARAM) ?? ""}
className="text-sm text-muted-foreground cursor-pointer" onChange={(value) =>
> handleDateFilterChange(DATE_START_PARAM, value)
Somente não pagos
</label>
<Switch
id="filter-nao-pago"
checked={
searchParams.get("settled") ===
SETTLED_FILTER_VALUES.UNPAID
} }
placeholder="Data inicial"
disabled={isPending} disabled={isPending}
onCheckedChange={(checked) => { inputClassName="border-dashed"
handleFilterChange( compact
"settled", />
checked ? SETTLED_FILTER_VALUES.UNPAID : null, <span className="hidden text-xs text-muted-foreground sm:block">
); até
}} </span>
<DatePicker
value={searchParams.get(DATE_END_PARAM) ?? ""}
onChange={(value) =>
handleDateFilterChange(DATE_END_PARAM, value)
}
placeholder="Data final"
disabled={isPending}
inputClassName="border-dashed"
compact
/> />
</div> </div>
</div> </div>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
Faixa de valor
</label>
<div className="flex items-center gap-2">
<Input
type="number"
inputMode="decimal"
min="0"
step="0.01"
placeholder="Mínimo"
aria-label="Valor mínimo"
value={valorMinValue}
onChange={(event) =>
setValorMinValue(event.target.value)
}
disabled={isPending}
className="text-sm border-dashed"
/>
<span className="text-xs text-muted-foreground">até</span>
<Input
type="number"
inputMode="decimal"
min="0"
step="0.01"
placeholder="Máximo"
aria-label="Valor máximo"
value={valorMaxValue}
onChange={(event) =>
setValorMaxValue(event.target.value)
}
disabled={isPending}
className="text-sm border-dashed"
/>
</div>
</div>
</div>
<Separator />
<div className="space-y-3">
<ToggleGroup
type="single"
value={settledFilterValue}
onValueChange={(value) => {
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"
>
<ToggleGroupItem
value={FILTER_EMPTY_VALUE}
className="text-xs font-medium transition-all data-[state=on]:border-foreground data-[state=on]:bg-foreground data-[state=on]:text-background data-[state=on]:shadow-sm"
>
Todos
</ToggleGroupItem>
<ToggleGroupItem
value={SETTLED_FILTER_VALUES.PAID}
className="text-xs font-medium transition-all data-[state=on]:border-foreground data-[state=on]:bg-foreground data-[state=on]:text-background data-[state=on]:shadow-sm"
>
Pagos
</ToggleGroupItem>
<ToggleGroupItem
value={SETTLED_FILTER_VALUES.UNPAID}
className="text-xs font-medium transition-all data-[state=on]:border-foreground data-[state=on]:bg-foreground data-[state=on]:text-background data-[state=on]:shadow-sm"
>
Não pagos
</ToggleGroupItem>
</ToggleGroup>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -824,14 +931,27 @@ export function TransactionsFilters({
</div> </div>
<DrawerFooter> <DrawerFooter>
<Button <div className="flex items-center justify-between gap-3 rounded-md border border-dashed px-3 py-2">
type="button" <span className="text-xs text-muted-foreground">
variant="outline" {hasActiveFilters
onClick={handleResetFilters} ? `${activeFilterCount} ${
disabled={isPending || !hasActiveFilters} activeFilterCount === 1
> ? "filtro ativo"
Limpar filtros : "filtros ativos"
</Button> }`
: "Nenhum filtro ativo"}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleResetFilters}
disabled={isPending || !hasActiveFilters}
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
>
Limpar
</Button>
</div>
</DrawerFooter> </DrawerFooter>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

View File

@@ -43,15 +43,34 @@ const loadPdfDeps = async () => {
return { jsPDF, autoTable }; return { jsPDF, autoTable };
}; };
const formatPeriodDate = (dateString: string) =>
formatDateOnly(dateString, {
day: "2-digit",
month: "2-digit",
year: "numeric",
}) ?? dateString;
export function TransactionsExport({ export function TransactionsExport({
lancamentos, lancamentos,
period, period,
exportContext, exportContext,
}: TransactionsExportProps) { }: TransactionsExportProps) {
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
const dateStartFilter = exportContext?.filters.dateStartFilter ?? null;
const dateEndFilter = exportContext?.filters.dateEndFilter ?? null;
const periodLabel =
dateStartFilter || dateEndFilter
? `${dateStartFilter ? formatPeriodDate(dateStartFilter) : "Início"} até ${
dateEndFilter ? formatPeriodDate(dateEndFilter) : "hoje"
}`
: displayPeriod(period);
const filePeriodSlug =
dateStartFilter || dateEndFilter
? `${dateStartFilter ?? "inicio"}-${dateEndFilter ?? "hoje"}`
: period;
const getFileName = (extension: string) => { const getFileName = (extension: string) => {
return `lancamentos-${period}.${extension}`; return `lancamentos-${filePeriodSlug}.${extension}`;
}; };
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
@@ -251,7 +270,7 @@ export function TransactionsExport({
doc.text("Lançamentos", titleX, 15); doc.text("Lançamentos", titleX, 15);
doc.setFontSize(10); doc.setFontSize(10);
doc.text(`Período: ${displayPeriod(period)}`, titleX, 22); doc.text(`Período: ${periodLabel}`, titleX, 22);
doc.text( doc.text(
`Gerado em: ${ `Gerado em: ${
formatDateTime(new Date(), { formatDateTime(new Date(), {

View File

@@ -33,3 +33,5 @@ export const SETTLED_FILTER_VALUES = {
export const AMOUNT_MIN_PARAM = "valorMin"; export const AMOUNT_MIN_PARAM = "valorMin";
export const AMOUNT_MAX_PARAM = "valorMax"; export const AMOUNT_MAX_PARAM = "valorMax";
export const DATE_START_PARAM = "dataInicio";
export const DATE_END_PARAM = "dataFim";

View File

@@ -11,6 +11,8 @@ type TransactionExportFilters = {
dividedFilter: string | null; dividedFilter: string | null;
amountMinFilter: number | null; amountMinFilter: number | null;
amountMaxFilter: number | null; amountMaxFilter: number | null;
dateStartFilter: string | null;
dateEndFilter: string | null;
}; };
export type TransactionsExportContext = { export type TransactionsExportContext = {

View File

@@ -22,6 +22,8 @@ import type { SelectOption } from "@/features/transactions/components/types";
import { import {
AMOUNT_MAX_PARAM, AMOUNT_MAX_PARAM,
AMOUNT_MIN_PARAM, AMOUNT_MIN_PARAM,
DATE_END_PARAM,
DATE_START_PARAM,
PAYMENT_METHODS, PAYMENT_METHODS,
SETTLED_FILTER_VALUES, SETTLED_FILTER_VALUES,
TRANSACTION_CONDITIONS, TRANSACTION_CONDITIONS,
@@ -38,7 +40,7 @@ import {
PAYER_ROLE_ADMIN, PAYER_ROLE_ADMIN,
PAYER_ROLE_THIRD_PARTY, PAYER_ROLE_THIRD_PARTY,
} from "@/shared/lib/payers/constants"; } from "@/shared/lib/payers/constants";
import { toDateOnlyString } from "@/shared/utils/date"; import { parseLocalDateString, toDateOnlyString } from "@/shared/utils/date";
import { slugify } from "@/shared/utils/string"; import { slugify } from "@/shared/utils/string";
type PayerRow = typeof payers.$inferSelect; type PayerRow = typeof payers.$inferSelect;
@@ -66,6 +68,8 @@ export type TransactionSearchFilters = {
dividedFilter: string | null; dividedFilter: string | null;
amountMinFilter: number | null; amountMinFilter: number | null;
amountMaxFilter: number | null; amountMaxFilter: number | null;
dateStartFilter: string | null;
dateEndFilter: string | null;
}; };
type BaseSluggedOption = { type BaseSluggedOption = {
@@ -162,6 +166,14 @@ export const parsePositiveAmount = (value: string | null): number | null => {
return Math.round(normalized * 100) / 100; return Math.round(normalized * 100) / 100;
}; };
export const parseDateFilterParam = (value: string | null): string | null => {
if (!value) return null;
const normalized = value.trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) return null;
const parsed = parseLocalDateString(normalized);
return Number.isNaN(parsed.getTime()) ? null : normalized;
};
export const extractTransactionSearchFilters = ( export const extractTransactionSearchFilters = (
params: ResolvedSearchParams, params: ResolvedSearchParams,
): TransactionSearchFilters => ({ ): TransactionSearchFilters => ({
@@ -181,6 +193,10 @@ export const extractTransactionSearchFilters = (
amountMaxFilter: parsePositiveAmount( amountMaxFilter: parsePositiveAmount(
getSingleParam(params, AMOUNT_MAX_PARAM), getSingleParam(params, AMOUNT_MAX_PARAM),
), ),
dateStartFilter: parseDateFilterParam(
getSingleParam(params, DATE_START_PARAM),
),
dateEndFilter: parseDateFilterParam(getSingleParam(params, DATE_END_PARAM)),
}); });
export const resolveTransactionPagination = ( export const resolveTransactionPagination = (
@@ -377,10 +393,29 @@ export const buildTransactionWhere = ({
accountId?: string; accountId?: string;
payerId?: string; payerId?: string;
}): SQL[] => { }): SQL[] => {
const where: SQL[] = [ const where: SQL[] = [eq(transactions.userId, userId)];
eq(transactions.userId, userId),
eq(transactions.period, period), if (filters.dateStartFilter || filters.dateEndFilter) {
]; if (filters.dateStartFilter) {
where.push(
gte(
transactions.purchaseDate,
parseLocalDateString(filters.dateStartFilter),
),
);
}
if (filters.dateEndFilter) {
where.push(
lte(
transactions.purchaseDate,
parseLocalDateString(filters.dateEndFilter),
),
);
}
} else {
where.push(eq(transactions.period, period));
}
if (payerId) { if (payerId) {
where.push(eq(transactions.payerId, payerId)); where.push(eq(transactions.payerId, payerId));

View File

@@ -75,6 +75,7 @@ export interface DatePickerProps {
required?: boolean; required?: boolean;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
inputClassName?: string;
/** Show compact format like "10 mar" instead of "10 de março de 2025" */ /** Show compact format like "10 mar" instead of "10 de março de 2025" */
compact?: boolean; compact?: boolean;
} }
@@ -87,6 +88,7 @@ export function DatePicker({
required = false, required = false,
disabled = false, disabled = false,
className, className,
inputClassName,
compact = false, compact = false,
}: DatePickerProps) { }: DatePickerProps) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -140,7 +142,7 @@ export function DatePicker({
id={id} id={id}
value={displayValue} value={displayValue}
placeholder={placeholder} placeholder={placeholder}
className="bg-background pr-10" className={cn("bg-background pr-10", inputClassName)}
onChange={handleInputChange} onChange={handleInputChange}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
required={required} required={required}