From 0171b0ce2f230bf348b40ec3bc8d18928974394b Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Thu, 28 May 2026 10:59:24 -0300 Subject: [PATCH] feat(lancamentos): refina filtros e tabela responsiva --- .../payers/lib/build-readonly-option-sets.ts | 4 + .../table/transaction-actions-menu.tsx | 157 +++++++++ .../table/transaction-settlement-button.tsx | 116 +++++++ .../components/table/transactions-columns.tsx | 233 +------------ .../components/table/transactions-filters.tsx | 81 ++++- .../table/transactions-mobile-list.tsx | 328 ++++++++++++++++++ .../components/table/transactions-table.tsx | 19 +- src/features/transactions/components/types.ts | 1 + src/features/transactions/lib/page-helpers.ts | 10 +- 9 files changed, 715 insertions(+), 234 deletions(-) create mode 100644 src/features/transactions/components/table/transaction-actions-menu.tsx create mode 100644 src/features/transactions/components/table/transaction-settlement-button.tsx create mode 100644 src/features/transactions/components/table/transactions-mobile-list.tsx diff --git a/src/features/payers/lib/build-readonly-option-sets.ts b/src/features/payers/lib/build-readonly-option-sets.ts index c30f4b9..2382922 100644 --- a/src/features/payers/lib/build-readonly-option-sets.ts +++ b/src/features/payers/lib/build-readonly-option-sets.ts @@ -50,7 +50,9 @@ export function buildReadOnlyOptionSets( categoriaOptionsMap.set(item.categoryId, { value: item.categoryId, label: normalizeOptionLabel(item.categoriaName, "Category"), + group: item.categoriaType, slug: item.categoryId, + icon: item.categoriaIcon, }); } }); @@ -67,6 +69,8 @@ export function buildReadOnlyOptionSets( (option) => ({ slug: option.value, label: option.label, + type: option.group, + icon: option.icon, }), ); diff --git a/src/features/transactions/components/table/transaction-actions-menu.tsx b/src/features/transactions/components/table/transaction-actions-menu.tsx new file mode 100644 index 0000000..985363f --- /dev/null +++ b/src/features/transactions/components/table/transaction-actions-menu.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { + RiCheckLine, + RiDeleteBin5Line, + RiFileCopyLine, + RiFileList2Line, + RiHistoryLine, + RiMoreFill, + RiPencilLine, + RiRefundLine, + RiTimeLine, +} from "@remixicon/react"; +import { Button } from "@/shared/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/shared/components/ui/dropdown-menu"; +import { REFUND_NOTE_PREFIX } from "@/shared/lib/accounts/constants"; +import type { TransactionItem } from "../types"; + +type TransactionActionsMenuProps = { + item: TransactionItem; + currentUserId: string; + onEdit?: (item: TransactionItem) => void; + onCopy?: (item: TransactionItem) => void; + onImport?: (item: TransactionItem) => void; + onConfirmDelete?: (item: TransactionItem) => void; + onViewDetails?: (item: TransactionItem) => void; + onRefund?: (item: TransactionItem) => void; + onAnticipate?: (item: TransactionItem) => void; + onViewAnticipationHistory?: (item: TransactionItem) => void; +}; + +export function TransactionActionsMenu({ + item, + currentUserId, + onEdit, + onCopy, + onImport, + onConfirmDelete, + onViewDetails, + onRefund, + onAnticipate, + onViewAnticipationHistory, +}: TransactionActionsMenuProps) { + const isOwnData = item.userId === currentUserId; + const canRefund = + isOwnData && + item.transactionType === "Despesa" && + item.condition === "À vista" && + !item.splitGroupId && + !item.readonly && + !item.note?.startsWith(REFUND_NOTE_PREFIX); + const showInstallmentActions = + isOwnData && item.condition === "Parcelado" && item.seriesId; + + return ( + + + + + + onViewDetails?.(item)} + disabled={!onViewDetails} + > + + Detalhes + + + {isOwnData ? ( + onEdit?.(item)} + disabled={item.readonly || !onEdit} + > + + Editar + + ) : null} + + {!item.readonly && isOwnData ? ( + onCopy?.(item)} disabled={!onCopy}> + + Copiar + + ) : null} + + {!item.readonly && !isOwnData ? ( + onImport?.(item)} + disabled={!onImport} + > + + Importar para Minha Conta + + ) : null} + + {canRefund ? ( + onRefund?.(item)} + disabled={!onRefund} + > + + Reembolso + + ) : null} + + {isOwnData ? ( + onConfirmDelete?.(item)} + disabled={item.readonly || !onConfirmDelete} + > + + Remover + + ) : null} + + {showInstallmentActions ? ( + <> + + + {!item.isAnticipated && onAnticipate ? ( + onAnticipate(item)}> + + Antecipar Parcelas + + ) : null} + + {onViewAnticipationHistory ? ( + onViewAnticipationHistory(item)} + > + + Histórico de Antecipações + + ) : null} + + {item.isAnticipated ? ( + + + Parcela Antecipada + + ) : null} + + ) : null} + + + ); +} diff --git a/src/features/transactions/components/table/transaction-settlement-button.tsx b/src/features/transactions/components/table/transaction-settlement-button.tsx new file mode 100644 index 0000000..0d87941 --- /dev/null +++ b/src/features/transactions/components/table/transaction-settlement-button.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { + RiBankCard2Line, + RiCheckboxBlankCircleLine, + RiCheckboxCircleFill, +} from "@remixicon/react"; +import { + CREDIT_CARD_PAYMENT_METHOD, + SETTLEABLE_PAYMENT_METHODS, +} from "@/features/transactions/lib/constants"; +import { Button } from "@/shared/components/ui/button"; +import { Spinner } from "@/shared/components/ui/spinner"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/shared/components/ui/tooltip"; +import { cn } from "@/shared/utils/ui"; +import type { TransactionItem } from "../types"; + +type TransactionSettlementButtonProps = { + item: TransactionItem; + isLoading: boolean; + onToggle?: (item: TransactionItem) => void; +}; + +export function TransactionSettlementButton({ + item, + isLoading, + onToggle, +}: TransactionSettlementButtonProps) { + const isCreditCard = item.paymentMethod === CREDIT_CARD_PAYMENT_METHOD; + const canToggleSettlement = ( + SETTLEABLE_PAYMENT_METHODS as readonly string[] + ).includes(item.paymentMethod); + + if (!canToggleSettlement && !isCreditCard) { + return null; + } + + if (isCreditCard) { + const invoicePaid = Boolean(item.isSettled); + + return ( + + + + + + + + {invoicePaid + ? "Fatura paga" + : "Lançamentos de cartão de crédito são liquidados ao pagar a fatura"} + + + ); + } + + const settled = Boolean(item.isSettled); + + return ( + + + + + + {settled ? "Desfazer pagamento" : "Marcar como pago"} + + + ); +} diff --git a/src/features/transactions/components/table/transactions-columns.tsx b/src/features/transactions/components/table/transactions-columns.tsx index ef3c79d..d51b06f 100644 --- a/src/features/transactions/components/table/transactions-columns.tsx +++ b/src/features/transactions/components/table/transactions-columns.tsx @@ -1,28 +1,13 @@ import { RiAttachment2, - RiBankCard2Line, RiChat1Line, - RiCheckboxBlankCircleLine, - RiCheckboxCircleFill, - RiCheckLine, - RiDeleteBin5Line, - RiFileCopyLine, - RiFileList2Line, RiGroupLine, - RiHistoryLine, - RiMoreFill, - RiPencilLine, - RiRefundLine, RiTimeLine, } from "@remixicon/react"; import type { ColumnDef } from "@tanstack/react-table"; import Image from "next/image"; import Link from "next/link"; import { DEFAULT_TRANSACTIONS_COLUMN_ORDER } from "@/features/transactions/lib/column-order"; -import { - CREDIT_CARD_PAYMENT_METHOD, - SETTLEABLE_PAYMENT_METHODS, -} from "@/features/transactions/lib/constants"; import { CategoryIconBadge, EstablishmentLogo, @@ -35,28 +20,20 @@ import { AvatarImage, } from "@/shared/components/ui/avatar"; import { Badge } from "@/shared/components/ui/badge"; -import { Button } from "@/shared/components/ui/button"; import { Checkbox } from "@/shared/components/ui/checkbox"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/shared/components/ui/dropdown-menu"; -import { Spinner } from "@/shared/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/shared/components/ui/tooltip"; -import { REFUND_NOTE_PREFIX } from "@/shared/lib/accounts/constants"; import { resolveLogoSrc } from "@/shared/lib/logo"; import { getAvatarSrc } from "@/shared/lib/payers/utils"; import { formatDate } from "@/shared/utils/date"; import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons"; import { cn } from "@/shared/utils/ui"; import type { TransactionItem } from "../types"; +import { TransactionActionsMenu } from "./transaction-actions-menu"; +import { TransactionSettlementButton } from "./transaction-settlement-button"; type BuildColumnsArgs = { currentUserId: string; @@ -551,195 +528,23 @@ function buildColumns({ enableSorting: false, cell: ({ row }) => (
- {(() => { - const paymentMethod = row.original.paymentMethod; - const isCreditCard = paymentMethod === CREDIT_CARD_PAYMENT_METHOD; - const canToggleSettlement = ( - SETTLEABLE_PAYMENT_METHODS as readonly string[] - ).includes(paymentMethod); - - if (!canToggleSettlement && !isCreditCard) return null; - - if (isCreditCard) { - const invoicePaid = Boolean(row.original.isSettled); - return ( - - - - - - - - {invoicePaid - ? "Fatura paga" - : "Lançamentos de cartão de crédito são liquidados ao pagar a fatura"} - - - ); - } - - const readOnly = row.original.readonly; - const loading = isSettlementLoading(row.original.id); - const settled = Boolean(row.original.isSettled); - - return ( - - - - - - {settled ? "Desfazer pagamento" : "Marcar como pago"} - - - ); - })()} - - - - - - - handleViewDetails(row.original)} - > - - Detalhes - - {row.original.userId === currentUserId && ( - handleEdit(row.original)} - disabled={row.original.readonly} - > - - Editar - - )} - {!row.original.readonly && - row.original.userId === currentUserId && ( - handleCopy(row.original)}> - - Copiar - - )} - {!row.original.readonly && - row.original.userId !== currentUserId && ( - handleImport(row.original)}> - - Importar para Minha Conta - - )} - {(() => { - const item = row.original; - const canRefund = - item.userId === currentUserId && - item.transactionType === "Despesa" && - item.condition === "À vista" && - !item.splitGroupId && - !item.readonly && - !item.note?.startsWith(REFUND_NOTE_PREFIX); - - if (!canRefund) return null; - - return ( - handleRefund(item)}> - - Reembolso - - ); - })()} - {row.original.userId === currentUserId && ( - handleConfirmDelete(row.original)} - disabled={row.original.readonly} - > - - Remover - - )} - - {row.original.userId === currentUserId && - row.original.condition === "Parcelado" && - row.original.seriesId && ( - <> - - - {!row.original.isAnticipated && onAnticipate && ( - handleAnticipate(row.original)} - > - - Antecipar Parcelas - - )} - - {onViewAnticipationHistory && ( - - handleViewAnticipationHistory(row.original) - } - > - - Histórico de Antecipações - - )} - - {row.original.isAnticipated && ( - - - Parcela Antecipada - - )} - - )} - - + +
), }); diff --git a/src/features/transactions/components/table/transactions-filters.tsx b/src/features/transactions/components/table/transactions-filters.tsx index 6eaf150..5756015 100644 --- a/src/features/transactions/components/table/transactions-filters.tsx +++ b/src/features/transactions/components/table/transactions-filters.tsx @@ -67,6 +67,7 @@ import { SelectTrigger, } from "@/shared/components/ui/select"; import { Separator } from "@/shared/components/ui/separator"; +import { Spinner } from "@/shared/components/ui/spinner"; import { Switch } from "@/shared/components/ui/switch"; import { ToggleGroup, @@ -193,6 +194,16 @@ type MultiOption = { render?: ReactNode; }; +const getCategoryFilterGroup = (type?: string | null) => { + if (type === "receita") { + return "Receitas"; + } + if (type === "despesa") { + return "Despesas"; + } + return "Outras"; +}; + interface MultiSelectFilterProps { placeholder: string; options: MultiOption[]; @@ -290,7 +301,10 @@ function MultiSelectFilter({ > {triggerLabel} - + @@ -331,7 +345,10 @@ function MultiSelectFilter({ {option.render ?? option.label} {isSelected ? ( - + ) : null} ); @@ -518,6 +535,7 @@ export function TransactionsFilters({ categoryOptions.map((option) => ({ value: option.slug, label: option.label, + group: getCategoryFilterGroup(option.type), render: ( ), @@ -585,6 +603,7 @@ export function TransactionsFilters({ return (
- + ) : null}
@@ -630,12 +649,19 @@ export function TransactionsFilters({ @@ -649,7 +675,7 @@ export function TransactionsFilters({ aria-label="Limpar filtros" className="text-xs text-muted-foreground hover:text-foreground h-9 px-2" > - + Limpar )} @@ -746,6 +772,7 @@ export function TransactionsFilters({ disabled={isPending} searchable searchPlaceholder="Buscar categoria..." + groupOrder={["Despesas", "Receitas", "Outras"]} /> @@ -775,7 +802,7 @@ export function TransactionsFilters({
{hasDateRangeFilter ? (