refactor(lancamentos): melhora layout e UX da tabela

- Adiciona coluna de categoria com ícone
- Move data de compra para dentro da célula de nome
- Simplifica exibição de pagador removendo Badge
- Refatora coluna conta/cartão com tooltips informativos
- Adiciona suporte a liquidação para Transferência bancária e Pré-Pago
- Remove imports não utilizados (RiBankCard2Line, RiBankLine)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-01-22 12:38:52 +00:00
parent a57c73bd11
commit 1deaa80f48

View File

@@ -1,4 +1,5 @@
"use client"; "use client";
import { CategoryIcon } from "@/components/categorias/category-icon";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import MoneyValues from "@/components/money-values"; import MoneyValues from "@/components/money-values";
import { TypeBadge } from "@/components/type-badge"; import { TypeBadge } from "@/components/type-badge";
@@ -45,8 +46,6 @@ import {
RiAddCircleFill, RiAddCircleFill,
RiAddCircleLine, RiAddCircleLine,
RiArrowLeftRightLine, RiArrowLeftRightLine,
RiBankCard2Line,
RiBankLine,
RiChat1Line, RiChat1Line,
RiCheckLine, RiCheckLine,
RiDeleteBin5Line, RiDeleteBin5Line,
@@ -69,6 +68,7 @@ import {
RowSelectionState, RowSelectionState,
SortingState, SortingState,
useReactTable, useReactTable,
VisibilityState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -152,13 +152,10 @@ const buildColumns = ({
enableHiding: false, enableHiding: false,
}, },
{ {
id: "purchaseDate",
accessorKey: "purchaseDate", accessorKey: "purchaseDate",
header: "Data", header: () => null,
cell: ({ row }) => ( cell: () => null,
<span className="whitespace-nowrap text-muted-foreground">
{formatDate(row.original.purchaseDate)}
</span>
),
}, },
{ {
accessorKey: "name", accessorKey: "name",
@@ -166,6 +163,7 @@ const buildColumns = ({
cell: ({ row }) => { cell: ({ row }) => {
const { const {
name, name,
purchaseDate,
installmentCount, installmentCount,
currentInstallment, currentInstallment,
paymentMethod, paymentMethod,
@@ -192,16 +190,21 @@ const buildColumns = ({
return ( return (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<EstabelecimentoLogo name={name} size={28} /> <EstabelecimentoLogo name={name} size={28} />
<Tooltip> <span className="flex flex-col">
<TooltipTrigger asChild> <span className="text-[11px] text-muted-foreground">
<span className="line-clamp-2 max-w-[160px] font-semibold truncate"> {formatDate(purchaseDate)}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span className="line-clamp-2 max-w-[160px] font-semibold truncate">
{name}
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
{name} {name}
</span> </TooltipContent>
</TooltipTrigger> </Tooltip>
<TooltipContent side="top" className="max-w-xs"> </span>
{name}
</TooltipContent>
</Tooltip>
{isDivided && ( {isDivided && (
<Tooltip> <Tooltip>
@@ -359,23 +362,37 @@ const buildColumns = ({
); );
}, },
}, },
{
accessorKey: "categoriaName",
header: "Categoria",
cell: ({ row }) => {
const { categoriaName, categoriaIcon } = row.original;
if (!categoriaName) {
return <span className="text-muted-foreground"></span>;
}
return (
<span className="flex items-center gap-2">
<CategoryIcon name={categoriaIcon} className="size-4" />
<span>{categoriaName}</span>
</span>
);
},
},
{ {
accessorKey: "pagadorName", accessorKey: "pagadorName",
header: "Pagador", header: "Pagador",
cell: ({ row }) => { cell: ({ row }) => {
const { pagadorId, pagadorName, pagadorAvatar } = row.original; const { pagadorId, pagadorName, pagadorAvatar } = row.original;
if (!pagadorName) {
return <Badge variant="outline"></Badge>;
}
const label = pagadorName.trim() || "Sem pagador"; const label = pagadorName.trim() || "Sem pagador";
const displayName = label.split(/\s+/)[0] ?? label; const displayName = label.split(/\s+/)[0] ?? label;
const avatarSrc = getAvatarSrc(pagadorAvatar); const avatarSrc = getAvatarSrc(pagadorAvatar);
const initial = displayName.charAt(0).toUpperCase() || "?"; const initial = displayName.charAt(0).toUpperCase() || "?";
const content = ( const content = (
<> <>
<Avatar className="size-6 border border-border/60 bg-background"> <Avatar className="size-7">
<AvatarImage src={avatarSrc} alt={`Avatar de ${label}`} /> <AvatarImage src={avatarSrc} alt={`Avatar de ${label}`} />
<AvatarFallback className="text-[10px] font-medium uppercase"> <AvatarFallback className="text-[10px] font-medium uppercase">
{initial} {initial}
@@ -387,30 +404,18 @@ const buildColumns = ({
if (!pagadorId) { if (!pagadorId) {
return ( return (
<Badge <span className="inline-flex items-center gap-2">{content}</span>
variant="outline"
className="max-w-[200px] px-2 py-0.5"
title={label}
>
<span className="inline-flex items-center gap-2">{content}</span>
</Badge>
); );
} }
return ( return (
<Badge <Link
asChild href={`/pagadores/${pagadorId}`}
variant="outline" className="inline-flex items-center gap-2 hover:underline"
className="max-w-[200px] px-2 py-0.5" title={label}
> >
<Link {content}
href={`/pagadores/${pagadorId}`} </Link>
className="inline-flex items-center gap-2"
title={label}
>
{content}
</Link>
</Badge>
); );
}, },
}, },
@@ -427,6 +432,7 @@ const buildColumns = ({
contaId, contaId,
userId, userId,
} = row.original; } = row.original;
const isCartao = Boolean(cartaoName);
const label = cartaoName ?? contaName; const label = cartaoName ?? contaName;
const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo); const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo);
const href = cartaoId const href = cartaoId
@@ -434,46 +440,57 @@ const buildColumns = ({
: contaId : contaId
? `/contas/${contaId}/extrato` ? `/contas/${contaId}/extrato`
: null; : null;
const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null;
const isOwnData = userId === currentUserId; const isOwnData = userId === currentUserId;
if (!label) {
return "—";
}
const content = ( const content = (
<> <span className="inline-flex items-center gap-2">
{logoSrc ? ( {logoSrc && (
<Image <Image
src={logoSrc} src={logoSrc}
alt={`Logo de ${label}`} alt={`Logo de ${label}`}
width={32} width={30}
height={32} height={30}
className="rounded-lg" className="rounded-md"
/> />
) : null} )}
<span className="truncate">{label}</span> <span className="truncate">{label}</span>
{Icon ? ( </span>
<Icon className="size-4 text-muted-foreground" aria-hidden />
) : null}
</>
); );
if (!isOwnData) { if (!isOwnData || !href) {
return <div className="flex items-center gap-2">{content}</div>; return (
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="top">
{isCartao ? "Cartão" : "Conta"}: {label}
</TooltipContent>
</Tooltip>
);
} }
return ( return (
<Link <Tooltip>
href={href ?? "#"} <TooltipTrigger asChild>
className={cn( <Link
"flex items-center gap-2", href={href}
href ? "underline " : "pointer-events-none", className="inline-flex items-center gap-2 hover:underline"
)} >
aria-disabled={!href} {logoSrc && (
> <Image
{content} src={logoSrc}
</Link> alt={`Logo de ${label}`}
width={30}
height={30}
className="rounded-md"
/>
)}
<span className="truncate">{label}</span>
</Link>
</TooltipTrigger>
<TooltipContent side="top">
{isCartao ? "Cartão" : "Conta"}: {label}
</TooltipContent>
</Tooltip>
); );
}, },
}, },
@@ -494,6 +511,8 @@ const buildColumns = ({
"Cartão de crédito", "Cartão de crédito",
"Dinheiro", "Dinheiro",
"Cartão de débito", "Cartão de débito",
"Transferência bancária",
"Pré-Pago | VR/VA",
].includes(paymentMethod); ].includes(paymentMethod);
if (!showSettlementButton) { if (!showSettlementButton) {
@@ -504,7 +523,9 @@ const buildColumns = ({
paymentMethod === "Pix" || paymentMethod === "Pix" ||
paymentMethod === "Boleto" || paymentMethod === "Boleto" ||
paymentMethod === "Dinheiro" || paymentMethod === "Dinheiro" ||
paymentMethod === "Cartão de débito"; paymentMethod === "Cartão de débito" ||
paymentMethod === "Transferência bancária" ||
paymentMethod === "Pré-Pago | VR/VA";
const readOnly = row.original.readonly; const readOnly = row.original.readonly;
const loading = isSettlementLoading(row.original.id); const loading = isSettlementLoading(row.original.id);
const settled = Boolean(row.original.isSettled); const settled = Boolean(row.original.isSettled);
@@ -512,11 +533,15 @@ const buildColumns = ({
return ( return (
<Button <Button
variant={settled ? "secondary" : "ghost"} variant={"outline"}
size="icon-sm" size="icon-sm"
onClick={() => handleToggleSettlement(row.original)} onClick={() => handleToggleSettlement(row.original)}
disabled={loading || readOnly || !canToggleSettlement} disabled={loading || readOnly || !canToggleSettlement}
className={canToggleSettlement ? undefined : "opacity-70"} className={cn(
"border-none",
!canToggleSettlement && "opacity-70 ",
settled && "border-none",
)}
> >
{loading ? ( {loading ? (
<Spinner className="size-4" /> <Spinner className="size-4" />
@@ -673,6 +698,9 @@ export function LancamentosTable({
const [sorting, setSorting] = useState<SortingState>([ const [sorting, setSorting] = useState<SortingState>([
{ id: "purchaseDate", desc: true }, { id: "purchaseDate", desc: true },
]); ]);
const [columnVisibility] = useState<VisibilityState>({
purchaseDate: false,
});
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
pageIndex: 0, pageIndex: 0,
pageSize: 30, pageSize: 30,
@@ -714,6 +742,7 @@ export function LancamentosTable({
columns, columns,
state: { state: {
sorting, sorting,
columnVisibility,
pagination, pagination,
rowSelection, rowSelection,
}, },