diff --git a/src/app/(dashboard)/payers/[payerId]/page.tsx b/src/app/(dashboard)/payers/[payerId]/page.tsx index f9e88aa..2e51e71 100644 --- a/src/app/(dashboard)/payers/[payerId]/page.tsx +++ b/src/app/(dashboard)/payers/[payerId]/page.tsx @@ -87,6 +87,8 @@ const EMPTY_FILTERS: TransactionSearchFilters = { dividedFilter: null, amountMinFilter: null, amountMaxFilter: null, + dateStartFilter: null, + dateEndFilter: null, }; const createEmptySlugMaps = (): SlugMaps => ({ diff --git a/src/features/transactions/actions/export-actions.ts b/src/features/transactions/actions/export-actions.ts index 914f203..3449eee 100644 --- a/src/features/transactions/actions/export-actions.ts +++ b/src/features/transactions/actions/export-actions.ts @@ -36,6 +36,14 @@ const exportTransactionsSchema: z.ZodType = z.object( dividedFilter: z.string().nullable(), amountMinFilter: 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(), cardId: z.string().min(1).nullable().optional(), diff --git a/src/features/transactions/components/table/transactions-filters.tsx b/src/features/transactions/components/table/transactions-filters.tsx index 8e76311..2cfdd6b 100644 --- a/src/features/transactions/components/table/transactions-filters.tsx +++ b/src/features/transactions/components/table/transactions-filters.tsx @@ -23,12 +23,17 @@ import { import { AMOUNT_MAX_PARAM, AMOUNT_MIN_PARAM, + DATE_END_PARAM, + DATE_START_PARAM, PAYMENT_METHODS, SETTLED_FILTER_VALUES, TRANSACTION_CONDITIONS, TRANSACTION_TYPES, } 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 { Checkbox } from "@/shared/components/ui/checkbox"; import { @@ -39,6 +44,7 @@ import { CommandItem, CommandList, } from "@/shared/components/ui/command"; +import { DatePicker } from "@/shared/components/ui/date-picker"; import { Drawer, DrawerContent, @@ -60,7 +66,12 @@ import { SelectItem, SelectTrigger, } from "@/shared/components/ui/select"; +import { Separator } from "@/shared/components/ui/separator"; import { Switch } from "@/shared/components/ui/switch"; +import { + ToggleGroup, + ToggleGroupItem, +} from "@/shared/components/ui/toggle-group"; import { slugify } from "@/shared/utils/string"; import { cn } from "@/shared/utils/ui"; import { @@ -83,6 +94,9 @@ const normalizeAmountParam = (raw: string): string | null => { return parsed === null ? null : parsed.toString(); }; +const normalizeDateParam = (raw: string): string | null => + parseDateFilterParam(raw.trim()); + function useDebouncedAmountFilter( param: string, searchParams: URLSearchParams | ReadonlyURLSearchParams, @@ -135,6 +149,7 @@ function FilterSelect({ value === FILTER_EMPTY_VALUE ? placeholder : (current?.label ?? placeholder); + const hasSelection = value !== FILTER_EMPTY_VALUE && Boolean(current); return ( setValorMinValue(event.target.value)} - disabled={isPending} - className="text-sm border-dashed" - /> - até - setValorMaxValue(event.target.value)} - disabled={isPending} - className="text-sm border-dashed" - /> +
+ + + handleMultiFilterChange("accountCard", values) + } + disabled={isPending} + searchable + searchPlaceholder="Buscar conta ou cartão..." + groupOrder={["Contas", "Cartões"]} + /> +
+ +
-

Status

-
-
-
+ + + +
+ { + 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 + +
@@ -824,14 +931,27 @@ export function TransactionsFilters({
- +
+ + {hasActiveFilters + ? `${activeFilterCount} ${ + activeFilterCount === 1 + ? "filtro ativo" + : "filtros ativos" + }` + : "Nenhum filtro ativo"} + + +
diff --git a/src/features/transactions/components/transactions-export.tsx b/src/features/transactions/components/transactions-export.tsx index dc22607..9d4bcd1 100644 --- a/src/features/transactions/components/transactions-export.tsx +++ b/src/features/transactions/components/transactions-export.tsx @@ -43,15 +43,34 @@ const loadPdfDeps = async () => { return { jsPDF, autoTable }; }; +const formatPeriodDate = (dateString: string) => + formatDateOnly(dateString, { + day: "2-digit", + month: "2-digit", + year: "numeric", + }) ?? dateString; + export function TransactionsExport({ lancamentos, period, exportContext, }: TransactionsExportProps) { 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) => { - return `lancamentos-${period}.${extension}`; + return `lancamentos-${filePeriodSlug}.${extension}`; }; const formatDate = (dateString: string) => { @@ -251,7 +270,7 @@ export function TransactionsExport({ doc.text("Lançamentos", titleX, 15); doc.setFontSize(10); - doc.text(`Período: ${displayPeriod(period)}`, titleX, 22); + doc.text(`Período: ${periodLabel}`, titleX, 22); doc.text( `Gerado em: ${ formatDateTime(new Date(), { diff --git a/src/features/transactions/lib/constants.ts b/src/features/transactions/lib/constants.ts index dc31685..1af585a 100644 --- a/src/features/transactions/lib/constants.ts +++ b/src/features/transactions/lib/constants.ts @@ -33,3 +33,5 @@ export const SETTLED_FILTER_VALUES = { export const AMOUNT_MIN_PARAM = "valorMin"; export const AMOUNT_MAX_PARAM = "valorMax"; +export const DATE_START_PARAM = "dataInicio"; +export const DATE_END_PARAM = "dataFim"; diff --git a/src/features/transactions/lib/export-types.ts b/src/features/transactions/lib/export-types.ts index d0ce0d9..24d65f5 100644 --- a/src/features/transactions/lib/export-types.ts +++ b/src/features/transactions/lib/export-types.ts @@ -11,6 +11,8 @@ type TransactionExportFilters = { dividedFilter: string | null; amountMinFilter: number | null; amountMaxFilter: number | null; + dateStartFilter: string | null; + dateEndFilter: string | null; }; export type TransactionsExportContext = { diff --git a/src/features/transactions/lib/page-helpers.ts b/src/features/transactions/lib/page-helpers.ts index 2684d03..0227852 100644 --- a/src/features/transactions/lib/page-helpers.ts +++ b/src/features/transactions/lib/page-helpers.ts @@ -22,6 +22,8 @@ import type { SelectOption } from "@/features/transactions/components/types"; import { AMOUNT_MAX_PARAM, AMOUNT_MIN_PARAM, + DATE_END_PARAM, + DATE_START_PARAM, PAYMENT_METHODS, SETTLED_FILTER_VALUES, TRANSACTION_CONDITIONS, @@ -38,7 +40,7 @@ import { PAYER_ROLE_ADMIN, PAYER_ROLE_THIRD_PARTY, } from "@/shared/lib/payers/constants"; -import { toDateOnlyString } from "@/shared/utils/date"; +import { parseLocalDateString, toDateOnlyString } from "@/shared/utils/date"; import { slugify } from "@/shared/utils/string"; type PayerRow = typeof payers.$inferSelect; @@ -66,6 +68,8 @@ export type TransactionSearchFilters = { dividedFilter: string | null; amountMinFilter: number | null; amountMaxFilter: number | null; + dateStartFilter: string | null; + dateEndFilter: string | null; }; type BaseSluggedOption = { @@ -162,6 +166,14 @@ export const parsePositiveAmount = (value: string | null): number | null => { 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 = ( params: ResolvedSearchParams, ): TransactionSearchFilters => ({ @@ -181,6 +193,10 @@ export const extractTransactionSearchFilters = ( amountMaxFilter: parsePositiveAmount( getSingleParam(params, AMOUNT_MAX_PARAM), ), + dateStartFilter: parseDateFilterParam( + getSingleParam(params, DATE_START_PARAM), + ), + dateEndFilter: parseDateFilterParam(getSingleParam(params, DATE_END_PARAM)), }); export const resolveTransactionPagination = ( @@ -377,10 +393,29 @@ export const buildTransactionWhere = ({ accountId?: string; payerId?: string; }): SQL[] => { - const where: SQL[] = [ - eq(transactions.userId, userId), - eq(transactions.period, period), - ]; + const where: SQL[] = [eq(transactions.userId, userId)]; + + 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) { where.push(eq(transactions.payerId, payerId)); diff --git a/src/shared/components/ui/date-picker.tsx b/src/shared/components/ui/date-picker.tsx index 9ab5a0a..ea2878d 100644 --- a/src/shared/components/ui/date-picker.tsx +++ b/src/shared/components/ui/date-picker.tsx @@ -75,6 +75,7 @@ export interface DatePickerProps { required?: boolean; disabled?: boolean; className?: string; + inputClassName?: string; /** Show compact format like "10 mar" instead of "10 de março de 2025" */ compact?: boolean; } @@ -87,6 +88,7 @@ export function DatePicker({ required = false, disabled = false, className, + inputClassName, compact = false, }: DatePickerProps) { const [open, setOpen] = React.useState(false); @@ -140,7 +142,7 @@ export function DatePicker({ id={id} value={displayValue} placeholder={placeholder} - className="bg-background pr-10" + className={cn("bg-background pr-10", inputClassName)} onChange={handleInputChange} onKeyDown={handleInputKeyDown} required={required}