"use client"; import { RiAddFill, RiArrowLeftDoubleLine, RiArrowLeftRightLine, RiArrowLeftSLine, RiArrowRightDoubleLine, RiArrowRightSLine, RiBankCard2Line, RiChat1Line, RiCheckboxBlankCircleLine, RiCheckboxCircleFill, RiCheckLine, RiDeleteBin5Line, RiFileCopyLine, RiFileExcel2Line, RiFileList2Line, RiFlashlightFill, RiGroupLine, RiHistoryLine, RiMoreFill, RiPencilLine, RiTimeLine, } from "@remixicon/react"; import { type ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, type RowSelectionState, type SortingState, useReactTable, type VisibilityState, } from "@tanstack/react-table"; import Image from "next/image"; import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useMemo, useState } from "react"; import { DEFAULT_LANCAMENTOS_COLUMN_ORDER } from "@/features/transactions/column-order"; import type { TransactionsExportContext, TransactionsPaginationState, } from "@/features/transactions/export-types"; import { EmptyState } from "@/shared/components/empty-state"; import { CategoryIconBadge, EstablishmentLogo, } from "@/shared/components/entity-avatar"; import MoneyValues from "@/shared/components/money-values"; import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge"; import { Avatar, AvatarFallback, AvatarImage, } from "@/shared/components/ui/avatar"; import { Badge } from "@/shared/components/ui/badge"; import { Button } from "@/shared/components/ui/button"; import { Card, CardContent } from "@/shared/components/ui/card"; import { Checkbox } from "@/shared/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/shared/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/shared/components/ui/select"; import { Spinner } from "@/shared/components/ui/spinner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/shared/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/shared/components/ui/tooltip"; 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 { TransactionsExport } from "../transactions-export"; import type { AccountCardFilterOption, TransactionFilterOption, TransactionItem, } from "../types"; import { TransactionsFilters } from "./transactions-filters"; type BuildColumnsArgs = { currentUserId: string; noteAsColumn: boolean; onEdit?: (item: TransactionItem) => void; onCopy?: (item: TransactionItem) => void; onImport?: (item: TransactionItem) => void; onConfirmDelete?: (item: TransactionItem) => void; onViewDetails?: (item: TransactionItem) => void; onToggleSettlement?: (item: TransactionItem) => void; onAnticipate?: (item: TransactionItem) => void; onViewAnticipationHistory?: (item: TransactionItem) => void; isSettlementLoading: (id: string) => boolean; showActions: boolean; }; const buildColumns = ({ currentUserId, noteAsColumn, onEdit, onCopy, onImport, onConfirmDelete, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading, showActions, }: BuildColumnsArgs): ColumnDef[] => { const noop = () => undefined; const handleEdit = onEdit ?? noop; const handleCopy = onCopy ?? noop; const handleImport = onImport ?? noop; const handleConfirmDelete = onConfirmDelete ?? noop; const handleViewDetails = onViewDetails ?? noop; const handleToggleSettlement = onToggleSettlement ?? noop; const handleAnticipate = onAnticipate ?? noop; const handleViewAnticipationHistory = onViewAnticipationHistory ?? noop; const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Selecionar todos" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="Selecionar linha" /> ), enableSorting: false, enableHiding: false, }, { id: "purchaseDate", accessorKey: "purchaseDate", header: () => null, cell: () => null, }, { accessorKey: "name", header: "Estabelecimento", cell: ({ row }) => { const { name, purchaseDate, installmentCount, currentInstallment, paymentMethod, dueDate, note, isDivided, isAnticipated, } = row.original; const installmentBadge = currentInstallment && installmentCount ? `${currentInstallment} de ${installmentCount}` : null; const isBoleto = paymentMethod === "Boleto" && dueDate; const dueDateLabel = isBoleto && dueDate ? `Venc. ${formatDate(dueDate)}` : null; const hasNote = Boolean(note?.trim().length); const isLastInstallment = currentInstallment === installmentCount && installmentCount && installmentCount > 1; return ( {formatDate(purchaseDate)} {name} {name} {isDivided && ( Dividido entre pagadores Dividido entre pagadores )} {isLastInstallment ? ( Última parcela Última parcela Última parcela! ) : null} {installmentBadge ? ( {installmentBadge} ) : null} {dueDateLabel ? ( {dueDateLabel} ) : null} {isAnticipated && ( Parcela antecipada Parcela antecipada )} {!noteAsColumn && hasNote ? ( Ver anotação {note} ) : null} ); }, }, { accessorKey: "transactionType", header: "Transação", cell: ({ row }) => { const type = row.original.categoriaName === "Saldo inicial" ? "Saldo inicial" : row.original.transactionType; return ( ); }, }, { accessorKey: "amount", header: "Valor", cell: ({ row }) => { const isReceita = row.original.transactionType === "Receita"; const isTransfer = row.original.transactionType === "Transferência"; return ( ); }, }, { accessorKey: "condition", header: "Condição", cell: ({ row }) => { const condition = row.original.condition; const icon = getConditionIcon(condition); return ( {icon} {condition} ); }, }, { accessorKey: "paymentMethod", header: "Forma de Pagamento", cell: ({ row }) => { const method = row.original.paymentMethod; const icon = getPaymentMethodIcon(method); return ( {icon} {method} ); }, }, { accessorKey: "categoriaName", header: "Categoria", cell: ({ row }) => { const { categoriaName, categoriaIcon } = row.original; if (!categoriaName) { return ; } return ( {categoriaName} ); }, }, { accessorKey: "pagadorName", header: "Pagador", cell: ({ row }) => { const { payerId, pagadorName, pagadorAvatar } = row.original; const label = pagadorName?.trim() || "Sem pagador"; const displayName = label.split(/\s+/)[0] ?? label; const avatarSrc = getAvatarSrc(pagadorAvatar); const initial = displayName.charAt(0).toUpperCase() || "?"; const content = ( <> {initial} {displayName} ); if (!payerId) { return ( {content} ); } return ( {content} ); }, }, { id: "contaCartao", header: "Conta/Cartão", cell: ({ row }) => { const { cartaoName, contaName, cartaoLogo, contaLogo, cardId, accountId, userId, } = row.original; const isCartao = Boolean(cartaoName); const label = cartaoName ?? contaName; const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo); const href = cardId ? `/cards/${cardId}/invoice` : accountId ? `/accounts/${accountId}/statement` : null; const isOwnData = userId === currentUserId; const content = ( {logoSrc && ( {`Logo )} {label} ); if (!isOwnData || !href) { return ( {content} {isCartao ? "Cartão" : "Conta"}: {label} ); } return ( {logoSrc && ( {`Logo )} {label} {isCartao ? "Cartão" : "Conta"}: {label} ); }, }, ]; if (noteAsColumn) { const accountCardIndex = columns.findIndex((c) => c.id === "contaCartao"); const noteColumn: ColumnDef = { accessorKey: "note", header: "Anotação", cell: ({ row }) => { const note = row.original.note; if (!note?.trim()) return ; return ( {note} ); }, }; columns.splice(accountCardIndex, 0, noteColumn); } if (showActions) { columns.push({ id: "actions", header: "Ações", enableSorting: false, cell: ({ row }) => (
{(() => { const paymentMethod = row.original.paymentMethod; const showSettlementButton = [ "Pix", "Boleto", "Cartão de crédito", "Dinheiro", "Cartão de débito", "Transferência bancária", "Pré-Pago | VR/VA", ].includes(paymentMethod); if (!showSettlementButton) { return null; } const canToggleSettlement = paymentMethod === "Pix" || paymentMethod === "Boleto" || paymentMethod === "Dinheiro" || paymentMethod === "Cartão de débito" || paymentMethod === "Transferência bancária" || paymentMethod === "Pré-Pago | VR/VA"; if (!canToggleSettlement) return ( ); 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.categoriaName !== "Pagamentos" && row.original.userId === currentUserId && ( handleCopy(row.original)}> Copiar )} {row.original.categoriaName !== "Pagamentos" && row.original.userId !== currentUserId && ( handleImport(row.original)}> Importar para Minha Conta )} {row.original.userId === currentUserId && ( handleConfirmDelete(row.original)} disabled={row.original.readonly} > Remover )} {/* Opções de Antecipação */} {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 )} )}
), }); } return columns; }; const FIXED_START_IDS = ["select", "purchaseDate"]; const FIXED_END_IDS = ["actions"]; function getColumnId(col: ColumnDef): string { const c = col as { id?: string; accessorKey?: string }; return c.id ?? c.accessorKey ?? ""; } function reorderColumnsByPreference( columns: ColumnDef[], orderPreference: string[] | null | undefined, ): ColumnDef[] { if (!orderPreference || orderPreference.length === 0) return columns; const order = orderPreference; const fixedStart: ColumnDef[] = []; const reorderable: ColumnDef[] = []; const fixedEnd: ColumnDef[] = []; for (const col of columns) { const id = getColumnId(col as ColumnDef); if (FIXED_START_IDS.includes(id)) fixedStart.push(col); else if (FIXED_END_IDS.includes(id)) fixedEnd.push(col); else reorderable.push(col); } const sorted = [...reorderable].sort((a, b) => { const idA = getColumnId(a as ColumnDef); const idB = getColumnId(b as ColumnDef); const indexA = order.indexOf(idA); const indexB = order.indexOf(idB); if (indexA === -1 && indexB === -1) return 0; if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; }); return [...fixedStart, ...sorted, ...fixedEnd]; } type LancamentosTableProps = { data: TransactionItem[]; currentUserId: string; noteAsColumn?: boolean; columnOrder?: string[] | null; payerFilterOptions?: TransactionFilterOption[]; categoryFilterOptions?: TransactionFilterOption[]; accountCardFilterOptions?: AccountCardFilterOption[]; selectedPeriod?: string; pagination?: TransactionsPaginationState; exportContext?: TransactionsExportContext; onCreate?: (type: "Despesa" | "Receita") => void; onMassAdd?: () => void; onEdit?: (item: TransactionItem) => void; onCopy?: (item: TransactionItem) => void; onImport?: (item: TransactionItem) => void; onConfirmDelete?: (item: TransactionItem) => void; onBulkDelete?: (items: TransactionItem[]) => void; onBulkImport?: (items: TransactionItem[]) => void; onViewDetails?: (item: TransactionItem) => void; onToggleSettlement?: (item: TransactionItem) => void; onAnticipate?: (item: TransactionItem) => void; onViewAnticipationHistory?: (item: TransactionItem) => void; isSettlementLoading?: (id: string) => boolean; showActions?: boolean; showFilters?: boolean; }; export function TransactionsTable({ data, currentUserId, noteAsColumn = false, columnOrder: columnOrderPreference = null, payerFilterOptions = [], categoryFilterOptions = [], accountCardFilterOptions = [], selectedPeriod, pagination: serverPagination, exportContext, onCreate, onMassAdd, onEdit, onCopy, onImport, onConfirmDelete, onBulkDelete, onBulkImport, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading, showActions = true, showFilters = true, }: LancamentosTableProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const [sorting, setSorting] = useState([ { id: "purchaseDate", desc: true }, ]); const [columnVisibility] = useState({ purchaseDate: false, }); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 30, }); const [rowSelection, setRowSelection] = useState({}); const isServerPaginated = Boolean(serverPagination); const columns = useMemo(() => { const built = buildColumns({ currentUserId, noteAsColumn, onEdit, onCopy, onImport, onConfirmDelete, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading: isSettlementLoading ?? (() => false), showActions, }); const order = columnOrderPreference?.length ? columnOrderPreference : DEFAULT_LANCAMENTOS_COLUMN_ORDER; return reorderColumnsByPreference(built, order); }, [ currentUserId, noteAsColumn, columnOrderPreference, onEdit, onCopy, onImport, onConfirmDelete, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading, showActions, ]); const table = useReactTable({ data, columns, state: isServerPaginated ? { sorting, columnVisibility, rowSelection, } : { sorting, columnVisibility, pagination, rowSelection, }, onSortingChange: setSorting, onPaginationChange: isServerPaginated ? undefined : setPagination, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: isServerPaginated ? undefined : getPaginationRowModel(), manualPagination: isServerPaginated, pageCount: serverPagination?.totalPages, enableRowSelection: true, }); const rowModel = table.getRowModel(); const hasRows = rowModel.rows.length > 0; const totalRows = isServerPaginated ? (serverPagination?.totalItems ?? 0) : table.getCoreRowModel().rows.length; const selectedRows = table.getFilteredSelectedRowModel().rows; const selectedCount = selectedRows.length; const selectedTotal = selectedRows.reduce( (total, row) => total + (row.original.amount ?? 0), 0, ); const currentPage = isServerPaginated ? (serverPagination?.page ?? 1) : table.getState().pagination.pageIndex + 1; const currentPageSize = isServerPaginated ? (serverPagination?.pageSize ?? pagination.pageSize) : pagination.pageSize; const totalPages = isServerPaginated ? Math.max(serverPagination?.totalPages ?? 1, 1) : Math.max(table.getPageCount(), 1); const canPreviousPage = currentPage > 1; const canNextPage = currentPage < totalPages; // Check if there's any data from other users const hasOtherUserData = data.some((item) => item.userId !== currentUserId); const handleBulkDelete = () => { if (onBulkDelete && selectedCount > 0) { const selectedItems = selectedRows.map((row) => row.original); onBulkDelete(selectedItems); setRowSelection({}); } }; const handleBulkImport = () => { if (onBulkImport && selectedCount > 0) { const selectedItems = selectedRows.map((row) => row.original); onBulkImport(selectedItems); setRowSelection({}); } }; const showTopControls = Boolean(onCreate) || Boolean(onMassAdd) || showFilters; const navigateToPage = (nextPage: number, nextPageSize = currentPageSize) => { const nextParams = new URLSearchParams(searchParams.toString()); if (nextPage <= 1) { nextParams.delete("page"); } else { nextParams.set("page", nextPage.toString()); } if (nextPageSize === 30) { nextParams.delete("pageSize"); } else { nextParams.set("pageSize", nextPageSize.toString()); } const target = nextParams.toString() ? `${pathname}?${nextParams.toString()}` : pathname; router.replace(target, { scroll: false }); setRowSelection({}); }; return ( {showTopControls ? (
{onCreate || onMassAdd ? (
{onCreate ? ( <> ) : null} {onMassAdd ? (

Adicionar múltiplos lançamentos

) : null}

Importar extrato

) : ( )} {showFilters ? ( ) : null } /> ) : null}
) : null} {selectedCount > 0 && onBulkDelete && selectedRows.every((row) => row.original.userId === currentUserId) ? (
{selectedCount}{" "} {selectedCount === 1 ? "item selecionado" : "itens selecionados"} - Total:{" "}
) : null} {selectedCount > 0 && onBulkImport && selectedRows.some((row) => row.original.userId !== currentUserId) ? (
{selectedCount}{" "} {selectedCount === 1 ? "item selecionado" : "itens selecionados"} - Total:{" "}
) : null} {hasRows ? ( <>
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ))} ))} {rowModel.rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))} ))}
{totalRows} lançamentos
Página {currentPage} de {totalPages}
) : (
} title="Nenhum lançamento encontrado" description="Ajuste os filtros ou cadastre um novo lançamento para visualizar aqui." />
)}
); }