refactor: atualiza transacoes dashboard e relatorios

This commit is contained in:
Felipe Coutinho
2026-03-14 12:51:22 +00:00
parent 43b0f0c47e
commit 6854017a8c
89 changed files with 2785 additions and 2705 deletions

View File

@@ -14,9 +14,9 @@ import {
useTransition,
} from "react";
import {
LANCAMENTO_CONDITIONS,
LANCAMENTO_PAYMENT_METHODS,
LANCAMENTO_TRANSACTION_TYPES,
PAYMENT_METHODS,
TRANSACTION_CONDITIONS,
TRANSACTION_TYPES,
} from "@/features/transactions/constants";
import { Button } from "@/shared/components/ui/button";
import {
@@ -52,14 +52,17 @@ import {
} from "@/shared/components/ui/select";
import { cn } from "@/shared/utils/ui";
import {
CategoriaSelectContent,
AccountCardSelectContent,
CategorySelectContent,
ConditionSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
PayerSelectContent,
PaymentMethodSelectContent,
TransactionTypeSelectContent,
} from "../select-items";
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
import type {
AccountCardFilterOption,
TransactionFilterOption,
} from "../types";
const FILTER_EMPTY_VALUE = "__all";
@@ -124,23 +127,23 @@ function FilterSelect({
);
}
interface LancamentosFiltersProps {
pagadorOptions: LancamentoFilterOption[];
categoriaOptions: LancamentoFilterOption[];
contaCartaoOptions: ContaCartaoFilterOption[];
interface TransactionsFiltersProps {
payerOptions: TransactionFilterOption[];
categoryOptions: TransactionFilterOption[];
accountCardOptions: AccountCardFilterOption[];
className?: string;
exportButton?: ReactNode;
hideAdvancedFilters?: boolean;
}
export function LancamentosFilters({
pagadorOptions,
categoriaOptions,
contaCartaoOptions,
export function TransactionsFilters({
payerOptions,
categoryOptions,
accountCardOptions,
className,
exportButton,
hideAdvancedFilters = false,
}: LancamentosFiltersProps) {
}: TransactionsFiltersProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -195,7 +198,7 @@ export function LancamentosFilters({
nextParams.set("periodo", periodValue);
}
setSearchValue("");
setCategoriaOpen(false);
setCategoryOpen(false);
startTransition(() => {
const target = nextParams.toString()
? `${pathname}?${nextParams.toString()}`
@@ -204,13 +207,13 @@ export function LancamentosFilters({
});
};
const pagadorSelectOptions = pagadorOptions.map((option) => ({
const payerSelectOptions = payerOptions.map((option) => ({
value: option.slug,
label: option.label,
avatarUrl: option.avatarUrl,
}));
const contaOptions = contaCartaoOptions
const accountOptions = accountCardOptions
.filter((option) => option.kind === "conta")
.map((option) => ({
value: option.slug,
@@ -218,7 +221,7 @@ export function LancamentosFilters({
logo: option.logo,
}));
const cartaoOptions = contaCartaoOptions
const cardOptions = accountCardOptions
.filter((option) => option.kind === "cartao")
.map((option) => ({
value: option.slug,
@@ -226,34 +229,34 @@ export function LancamentosFilters({
logo: option.logo,
}));
const categoriaValue = getParamValue("categoria");
const selectedCategoria =
categoriaValue !== FILTER_EMPTY_VALUE
? categoriaOptions.find((option) => option.slug === categoriaValue)
const categoryValue = getParamValue("category");
const selectedCategory =
categoryValue !== FILTER_EMPTY_VALUE
? categoryOptions.find((option) => option.slug === categoryValue)
: null;
const pagadorValue = getParamValue("pagador");
const selectedPagador =
pagadorValue !== FILTER_EMPTY_VALUE
? pagadorOptions.find((option) => option.slug === pagadorValue)
const payerValue = getParamValue("payer");
const selectedPayer =
payerValue !== FILTER_EMPTY_VALUE
? payerOptions.find((option) => option.slug === payerValue)
: null;
const contaCartaoValue = getParamValue("contaCartao");
const selectedContaCartao =
contaCartaoValue !== FILTER_EMPTY_VALUE
? contaCartaoOptions.find((option) => option.slug === contaCartaoValue)
const accountCardValue = getParamValue("accountCard");
const selectedAccountCard =
accountCardValue !== FILTER_EMPTY_VALUE
? accountCardOptions.find((option) => option.slug === accountCardValue)
: null;
const [categoriaOpen, setCategoriaOpen] = useState(false);
const [categoryOpen, setCategoryOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const hasActiveFilters =
searchParams.get("transacao") ||
searchParams.get("condicao") ||
searchParams.get("pagamento") ||
searchParams.get("pagador") ||
searchParams.get("categoria") ||
searchParams.get("contaCartao");
searchParams.get("type") ||
searchParams.get("condition") ||
searchParams.get("payment") ||
searchParams.get("payer") ||
searchParams.get("category") ||
searchParams.get("accountCard");
const handleResetFilters = () => {
handleReset();
@@ -315,9 +318,9 @@ export function LancamentosFilters({
Tipo de Lançamento
</label>
<FilterSelect
param="transacao"
param="type"
placeholder="Todos"
options={buildStaticOptions(LANCAMENTO_TRANSACTION_TYPES)}
options={buildStaticOptions(TRANSACTION_TYPES)}
widthClass="w-full border-dashed"
disabled={isPending}
getParamValue={getParamValue}
@@ -333,9 +336,9 @@ export function LancamentosFilters({
Condição de Lançamento
</label>
<FilterSelect
param="condicao"
param="condition"
placeholder="Todas"
options={buildStaticOptions(LANCAMENTO_CONDITIONS)}
options={buildStaticOptions(TRANSACTION_CONDITIONS)}
widthClass="w-full border-dashed"
disabled={isPending}
getParamValue={getParamValue}
@@ -351,9 +354,9 @@ export function LancamentosFilters({
Forma de Pagamento
</label>
<FilterSelect
param="pagamento"
param="payment"
placeholder="Todos"
options={buildStaticOptions(LANCAMENTO_PAYMENT_METHODS)}
options={buildStaticOptions(PAYMENT_METHODS)}
widthClass="w-full border-dashed"
disabled={isPending}
getParamValue={getParamValue}
@@ -365,12 +368,12 @@ export function LancamentosFilters({
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Pagador</label>
<label className="text-sm font-medium">Payer</label>
<Select
value={getParamValue("pagador")}
value={getParamValue("payer")}
onValueChange={(value) =>
handleFilterChange(
"pagador",
"payer",
value === FILTER_EMPTY_VALUE ? null : value,
)
}
@@ -381,10 +384,10 @@ export function LancamentosFilters({
disabled={isPending}
>
<span className="truncate">
{selectedPagador ? (
<PagadorSelectContent
label={selectedPagador.label}
avatarUrl={selectedPagador.avatarUrl}
{selectedPayer ? (
<PayerSelectContent
label={selectedPayer.label}
avatarUrl={selectedPayer.avatarUrl}
/>
) : (
"Todos"
@@ -393,9 +396,9 @@ export function LancamentosFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
{pagadorSelectOptions.map((option) => (
{payerSelectOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -406,25 +409,25 @@ export function LancamentosFilters({
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Categoria</label>
<label className="text-sm font-medium">Category</label>
<Popover
open={categoriaOpen}
onOpenChange={setCategoriaOpen}
open={categoryOpen}
onOpenChange={setCategoryOpen}
modal
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={categoriaOpen}
aria-expanded={categoryOpen}
className="w-full justify-between text-sm border-dashed"
disabled={isPending}
>
<span className="truncate flex items-center gap-2">
{selectedCategoria ? (
<CategoriaSelectContent
label={selectedCategoria.label}
icon={selectedCategoria.icon}
{selectedCategory ? (
<CategorySelectContent
label={selectedCategory.label}
icon={selectedCategory.icon}
/>
) : (
"Todas"
@@ -442,29 +445,29 @@ export function LancamentosFilters({
<CommandItem
value={FILTER_EMPTY_VALUE}
onSelect={() => {
handleFilterChange("categoria", null);
setCategoriaOpen(false);
handleFilterChange("category", null);
setCategoryOpen(false);
}}
>
Todas
{categoriaValue === FILTER_EMPTY_VALUE ? (
{categoryValue === FILTER_EMPTY_VALUE ? (
<RiCheckLine className="ml-auto size-4" />
) : null}
</CommandItem>
{categoriaOptions.map((option) => (
{categoryOptions.map((option) => (
<CommandItem
key={option.slug}
value={option.slug}
onSelect={() => {
handleFilterChange("categoria", option.slug);
setCategoriaOpen(false);
handleFilterChange("category", option.slug);
setCategoryOpen(false);
}}
>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>
{categoriaValue === option.slug ? (
{categoryValue === option.slug ? (
<RiCheckLine className="ml-auto size-4" />
) : null}
</CommandItem>
@@ -479,10 +482,10 @@ export function LancamentosFilters({
<div className="space-y-2">
<label className="text-sm font-medium">Conta/Cartão</label>
<Select
value={getParamValue("contaCartao")}
value={getParamValue("accountCard")}
onValueChange={(value) =>
handleFilterChange(
"contaCartao",
"accountCard",
value === FILTER_EMPTY_VALUE ? null : value,
)
}
@@ -493,11 +496,11 @@ export function LancamentosFilters({
disabled={isPending}
>
<span className="truncate">
{selectedContaCartao ? (
<ContaCartaoSelectContent
label={selectedContaCartao.label}
logo={selectedContaCartao.logo}
isCartao={selectedContaCartao.kind === "cartao"}
{selectedAccountCard ? (
<AccountCardSelectContent
label={selectedAccountCard.label}
logo={selectedAccountCard.logo}
isCartao={selectedAccountCard.kind === "cartao"}
/>
) : (
"Todos"
@@ -506,12 +509,12 @@ export function LancamentosFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
{contaOptions.length > 0 ? (
{accountOptions.length > 0 ? (
<SelectGroup>
<SelectLabel>Contas</SelectLabel>
{contaOptions.map((option) => (
{accountOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -520,12 +523,12 @@ export function LancamentosFilters({
))}
</SelectGroup>
) : null}
{cartaoOptions.length > 0 ? (
{cardOptions.length > 0 ? (
<SelectGroup>
<SelectLabel>Cartões</SelectLabel>
{cartaoOptions.map((option) => (
{cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}

View File

@@ -79,25 +79,25 @@ import { formatDate } from "@/shared/utils/date";
import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons";
import { cn } from "@/shared/utils/ui";
import { EstabelecimentoLogo } from "../shared/establishment-logo";
import { LancamentosExport } from "../transactions-export";
import { TransactionsExport } from "../transactions-export";
import type {
ContaCartaoFilterOption,
LancamentoFilterOption,
LancamentoItem,
AccountCardFilterOption,
TransactionFilterOption,
TransactionItem,
} from "../types";
import { LancamentosFilters } from "./transactions-filters";
import { TransactionsFilters } from "./transactions-filters";
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;
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;
};
@@ -115,7 +115,7 @@ const buildColumns = ({
onViewAnticipationHistory,
isSettlementLoading,
showActions,
}: BuildColumnsArgs): ColumnDef<LancamentoItem>[] => {
}: BuildColumnsArgs): ColumnDef<TransactionItem>[] => {
const noop = () => undefined;
const handleEdit = onEdit ?? noop;
const handleCopy = onCopy ?? noop;
@@ -126,7 +126,7 @@ const buildColumns = ({
const handleAnticipate = onAnticipate ?? noop;
const handleViewAnticipationHistory = onViewAnticipationHistory ?? noop;
const columns: ColumnDef<LancamentoItem>[] = [
const columns: ColumnDef<TransactionItem>[] = [
{
id: "select",
header: ({ table }) => (
@@ -380,7 +380,7 @@ const buildColumns = ({
accessorKey: "pagadorName",
header: "Pagador",
cell: ({ row }) => {
const { pagadorId, pagadorName, pagadorAvatar } = row.original;
const { payerId, pagadorName, pagadorAvatar } = row.original;
const label = pagadorName?.trim() || "Sem pagador";
const displayName = label.split(/\s+/)[0] ?? label;
@@ -398,7 +398,7 @@ const buildColumns = ({
</>
);
if (!pagadorId) {
if (!payerId) {
return (
<span className="inline-flex items-center gap-2">{content}</span>
);
@@ -406,7 +406,7 @@ const buildColumns = ({
return (
<Link
href={`/payers/${pagadorId}`}
href={`/payers/${payerId}`}
className="inline-flex items-center gap-2 hover:underline"
title={label}
>
@@ -424,17 +424,17 @@ const buildColumns = ({
contaName,
cartaoLogo,
contaLogo,
cartaoId,
contaId,
cardId,
accountId,
userId,
} = row.original;
const isCartao = Boolean(cartaoName);
const label = cartaoName ?? contaName;
const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo);
const href = cartaoId
? `/cards/${cartaoId}/invoice`
: contaId
? `/accounts/${contaId}/statement`
const href = cardId
? `/cards/${cardId}/invoice`
: accountId
? `/accounts/${accountId}/statement`
: null;
const isOwnData = userId === currentUserId;
@@ -458,7 +458,7 @@ const buildColumns = ({
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="top">
{isCartao ? "Cartão" : "Conta"}: {label}
{isCartao ? "Cartão" : "FinancialAccount"}: {label}
</TooltipContent>
</Tooltip>
);
@@ -484,7 +484,7 @@ const buildColumns = ({
</Link>
</TooltipTrigger>
<TooltipContent side="top">
{isCartao ? "Cartão" : "Conta"}: {label}
{isCartao ? "Cartão" : "FinancialAccount"}: {label}
</TooltipContent>
</Tooltip>
);
@@ -493,8 +493,8 @@ const buildColumns = ({
];
if (noteAsColumn) {
const contaCartaoIndex = columns.findIndex((c) => c.id === "contaCartao");
const noteColumn: ColumnDef<LancamentoItem> = {
const accountCardIndex = columns.findIndex((c) => c.id === "contaCartao");
const noteColumn: ColumnDef<TransactionItem> = {
accessorKey: "note",
header: "Anotação",
cell: ({ row }) => {
@@ -511,7 +511,7 @@ const buildColumns = ({
);
},
};
columns.splice(contaCartaoIndex, 0, noteColumn);
columns.splice(accountCardIndex, 0, noteColumn);
}
if (showActions) {
@@ -607,7 +607,7 @@ const buildColumns = ({
row.original.userId !== currentUserId && (
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
<RiFileCopyLine className="size-4" />
Importar para Minha Conta
Importar para Minha FinancialAccount
</DropdownMenuItem>
)}
{row.original.userId === currentUserId && (
@@ -669,7 +669,7 @@ const buildColumns = ({
const FIXED_START_IDS = ["select", "purchaseDate"];
const FIXED_END_IDS = ["actions"];
function getColumnId(col: ColumnDef<LancamentoItem>): string {
function getColumnId(col: ColumnDef<TransactionItem>): string {
const c = col as { id?: string; accessorKey?: string };
return c.id ?? c.accessorKey ?? "";
}
@@ -686,15 +686,15 @@ function reorderColumnsByPreference<T>(
const fixedEnd: ColumnDef<T>[] = [];
for (const col of columns) {
const id = getColumnId(col as ColumnDef<LancamentoItem>);
const id = getColumnId(col as ColumnDef<TransactionItem>);
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<LancamentoItem>);
const idB = getColumnId(b as ColumnDef<LancamentoItem>);
const idA = getColumnId(a as ColumnDef<TransactionItem>);
const idB = getColumnId(b as ColumnDef<TransactionItem>);
const indexA = order.indexOf(idA);
const indexB = order.indexOf(idB);
if (indexA === -1 && indexB === -1) return 0;
@@ -707,39 +707,39 @@ function reorderColumnsByPreference<T>(
}
type LancamentosTableProps = {
data: LancamentoItem[];
data: TransactionItem[];
currentUserId: string;
noteAsColumn?: boolean;
columnOrder?: string[] | null;
pagadorFilterOptions?: LancamentoFilterOption[];
categoriaFilterOptions?: LancamentoFilterOption[];
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
payerFilterOptions?: TransactionFilterOption[];
categoryFilterOptions?: TransactionFilterOption[];
accountCardFilterOptions?: AccountCardFilterOption[];
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;
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 LancamentosTable({
export function TransactionsTable({
data,
currentUserId,
noteAsColumn = false,
columnOrder: columnOrderPreference = null,
pagadorFilterOptions = [],
categoriaFilterOptions = [],
contaCartaoFilterOptions = [],
payerFilterOptions = [],
categoryFilterOptions = [],
accountCardFilterOptions = [],
selectedPeriod,
onCreate,
onMassAdd,
@@ -904,15 +904,15 @@ export function LancamentosTable({
)}
{showFilters ? (
<LancamentosFilters
pagadorOptions={pagadorFilterOptions}
categoriaOptions={categoriaFilterOptions}
contaCartaoOptions={contaCartaoFilterOptions}
<TransactionsFilters
payerOptions={payerFilterOptions}
categoryOptions={categoryFilterOptions}
accountCardOptions={accountCardFilterOptions}
className="w-full lg:flex-1 lg:justify-end"
hideAdvancedFilters={hasOtherUserData}
exportButton={
selectedPeriod ? (
<LancamentosExport
<TransactionsExport
lancamentos={data}
period={selectedPeriod}
/>