"use client"; import { RiAddCircleFill, RiAddCircleLine, RiArrowLeftRightLine, RiChat1Line, RiCheckLine, RiDeleteBin5Line, RiEyeLine, RiFileCopyLine, RiGroupLine, RiHistoryLine, RiMoreFill, RiPencilLine, RiThumbUpFill, RiThumbUpLine, 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 { useMemo, useState } from "react"; import { CategoryIcon } from "@/components/categorias/category-icon"; 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 { DEFAULT_LANCAMENTOS_COLUMN_ORDER } from "@/lib/lancamentos/column-order"; 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 { LancamentosExport } from "../lancamentos-export"; 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 = { currentUserId: string; noteAsColumn: boolean; onEdit?: (item: LancamentoItem) => void; onCopy?: (item: LancamentoItem) => void; onImport?: (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 = ({ 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 { pagadorId, 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 (!pagadorId) { return ( {content} ); } return ( {content} ); }, }, { id: "contaCartao", header: "Conta/Cartão", cell: ({ row }) => { const { cartaoName, contaName, cartaoLogo, contaLogo, cartaoId, contaId, userId, } = row.original; const isCartao = Boolean(cartaoName); const label = cartaoName ?? contaName; const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo); const href = cartaoId ? `/cartoes/${cartaoId}/fatura` : contaId ? `/contas/${contaId}/extrato` : 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 contaCartaoIndex = 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(contaCartaoIndex, 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"; 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 {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: LancamentoItem[]; currentUserId: string; noteAsColumn?: boolean; columnOrder?: string[] | null; pagadorFilterOptions?: LancamentoFilterOption[]; categoriaFilterOptions?: LancamentoFilterOption[]; contaCartaoFilterOptions?: ContaCartaoFilterOption[]; selectedPeriod?: string; onCreate?: (type: "Despesa" | "Receita") => void; onMassAdd?: () => void; onEdit?: (item: LancamentoItem) => void; onCopy?: (item: LancamentoItem) => void; onImport?: (item: LancamentoItem) => void; onConfirmDelete?: (item: LancamentoItem) => void; onBulkDelete?: (items: LancamentoItem[]) => void; onBulkImport?: (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, currentUserId, noteAsColumn = false, columnOrder: columnOrderPreference = null, pagadorFilterOptions = [], categoriaFilterOptions = [], contaCartaoFilterOptions = [], selectedPeriod, onCreate, onMassAdd, onEdit, onCopy, onImport, onConfirmDelete, onBulkDelete, onBulkImport, onViewDetails, onToggleSettlement, onAnticipate, onViewAnticipationHistory, isSettlementLoading, showActions = true, showFilters = true, }: LancamentosTableProps) { 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 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: { sorting, columnVisibility, 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, ); // 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; return ( {showTopControls ? (
{onCreate || onMassAdd ? (
{onCreate ? ( <> ) : null} {onMassAdd ? (

Adicionar múltiplos lançamentos

) : null}
) : ( )} {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(), )} ))} ))}
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." />
)}
); }