"use client"; import { EmptyState } from "@/components/empty-state"; import MoneyValues from "@/components/money-values"; import { TypeBadge } from "@/components/type-badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Spinner } from "@/components/ui/spinner"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { getAvatarSrc } from "@/lib/pagadores/utils"; import { formatDate } from "@/lib/utils/date"; import { getConditionIcon, getPaymentMethodIcon } from "@/lib/utils/icons"; import { cn } from "@/lib/utils/ui"; import { title_font } from "@/public/fonts/font_index"; import { RiAddCircleFill, RiAddCircleLine, RiArrowLeftRightLine, RiBankCard2Line, RiBankLine, RiChat1Line, RiCheckLine, RiDeleteBin5Line, RiEyeLine, RiFileCopyLine, RiGroupLine, RiHistoryLine, RiMoreFill, RiPencilLine, RiThumbUpFill, RiThumbUpLine, RiTimeLine, } from "@remixicon/react"; import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, RowSelectionState, SortingState, useReactTable, } from "@tanstack/react-table"; import Image from "next/image"; import Link from "next/link"; import { useMemo, useState } from "react"; import { EstabelecimentoLogo } from "../shared/estabelecimento-logo"; import type { ContaCartaoFilterOption, LancamentoFilterOption, LancamentoItem, } from "../types"; import { LancamentosFilters } from "./lancamentos-filters"; const resolveLogoSrc = (logo: string | null) => { if (!logo) { return null; } const fileName = logo.split("/").filter(Boolean).pop() ?? logo; return `/logos/${fileName}`; }; type BuildColumnsArgs = { onEdit?: (item: LancamentoItem) => void; onCopy?: (item: LancamentoItem) => void; onConfirmDelete?: (item: LancamentoItem) => void; onViewDetails?: (item: LancamentoItem) => void; onToggleSettlement?: (item: LancamentoItem) => void; onAnticipate?: (item: LancamentoItem) => void; onViewAnticipationHistory?: (item: LancamentoItem) => void; isSettlementLoading: (id: string) => boolean; showActions: boolean; }; const buildColumns = ({ onEdit, onCopy, onConfirmDelete, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading, showActions, }: BuildColumnsArgs): ColumnDef[] => { const noop = () => undefined; const handleEdit = onEdit ?? noop; const handleCopy = onCopy ?? 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, }, { accessorKey: "purchaseDate", header: "Data", cell: ({ row }) => ( {formatDate(row.original.purchaseDate)} ), }, { accessorKey: "name", header: "Estabelecimento", cell: ({ row }) => { const { name, 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 ( {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 )} {hasNote ? ( Ver anotação {note} ) : null} ); }, }, { accessorKey: "transactionType", header: "Transação", cell: ({ row }) => ( ), }, { 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: "pagadorName", header: "Pagador", cell: ({ row }) => { const { pagadorId, pagadorName, pagadorAvatar } = row.original; if (!pagadorName) { return ; } 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 (!pagadorId) { return ( {content} ); } return ( {content} ); }, }, { id: "contaCartao", header: "Conta/Cartão", cell: ({ row }) => { const { cartaoName, contaName, cartaoLogo, contaLogo, cartaoId, contaId, } = row.original; const label = cartaoName ?? contaName; const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo); const href = cartaoId ? `/cartoes/${cartaoId}/fatura` : contaId ? `/contas/${contaId}/extrato` : null; const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null; if (!label) { return "—"; } return ( {logoSrc ? ( {`Logo ) : null} {label} {Icon ? ( ) : null} ); }, }, ]; 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", ].includes(paymentMethod); if (!showSettlementButton) { return null; } const canToggleSettlement = paymentMethod === "Pix" || paymentMethod === "Boleto" || paymentMethod === "Dinheiro" || paymentMethod === "Cartão de débito"; const readOnly = row.original.readonly; const loading = isSettlementLoading(row.original.id); const settled = Boolean(row.original.isSettled); const Icon = settled ? RiThumbUpFill : RiThumbUpLine; return ( ); })()} handleViewDetails(row.original)} > Detalhes handleEdit(row.original)} disabled={row.original.readonly} > Editar {row.original.categoriaName !== "Pagamentos" && ( handleCopy(row.original)}> Copiar )} handleConfirmDelete(row.original)} disabled={row.original.readonly} > Remover {/* Opções de Antecipação */} {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; }; type LancamentosTableProps = { data: LancamentoItem[]; pagadorFilterOptions?: LancamentoFilterOption[]; categoriaFilterOptions?: LancamentoFilterOption[]; contaCartaoFilterOptions?: ContaCartaoFilterOption[]; onCreate?: () => void; onMassAdd?: () => void; onEdit?: (item: LancamentoItem) => void; onCopy?: (item: LancamentoItem) => void; onConfirmDelete?: (item: LancamentoItem) => void; onBulkDelete?: (items: LancamentoItem[]) => void; onViewDetails?: (item: LancamentoItem) => void; onToggleSettlement?: (item: LancamentoItem) => void; onAnticipate?: (item: LancamentoItem) => void; onViewAnticipationHistory?: (item: LancamentoItem) => void; isSettlementLoading?: (id: string) => boolean; showActions?: boolean; showFilters?: boolean; }; export function LancamentosTable({ data, pagadorFilterOptions = [], categoriaFilterOptions = [], contaCartaoFilterOptions = [], onCreate, onMassAdd, onEdit, onCopy, onConfirmDelete, onBulkDelete, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading, showActions = true, showFilters = true, }: LancamentosTableProps) { const [sorting, setSorting] = useState([ { id: "purchaseDate", desc: true }, ]); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 30, }); const [rowSelection, setRowSelection] = useState({}); const columns = useMemo( () => buildColumns({ onEdit, onCopy, onConfirmDelete, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading: isSettlementLoading ?? (() => false), showActions, }), [ onEdit, onCopy, onConfirmDelete, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading, showActions, ] ); const table = useReactTable({ data, columns, state: { sorting, pagination, rowSelection, }, onSortingChange: setSorting, onPaginationChange: setPagination, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), enableRowSelection: true, }); const rowModel = table.getRowModel(); const hasRows = rowModel.rows.length > 0; const totalRows = 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 handleBulkDelete = () => { if (onBulkDelete && selectedCount > 0) { const selectedItems = selectedRows.map((row) => row.original); onBulkDelete(selectedItems); setRowSelection({}); } }; const showTopControls = Boolean(onCreate) || Boolean(onMassAdd) || showFilters; return ( {showTopControls ? (
{onCreate || onMassAdd ? (
{onCreate ? ( ) : null} {onMassAdd ? ( ) : null}
) : ( )} {showFilters ? ( ) : null}
) : null} {selectedCount > 0 && onBulkDelete ? (
{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() )} ))} ))}
Exibindo {rowModel.rows.length} de {totalRows} lançamentos
) : (
} title="Nenhum lançamento encontrado" description="Ajuste os filtros ou cadastre um novo lançamento para visualizar aqui." />
)}
); }