diff --git a/src/features/payers/lib/build-readonly-option-sets.ts b/src/features/payers/lib/build-readonly-option-sets.ts
index c30f4b9..2382922 100644
--- a/src/features/payers/lib/build-readonly-option-sets.ts
+++ b/src/features/payers/lib/build-readonly-option-sets.ts
@@ -50,7 +50,9 @@ export function buildReadOnlyOptionSets(
categoriaOptionsMap.set(item.categoryId, {
value: item.categoryId,
label: normalizeOptionLabel(item.categoriaName, "Category"),
+ group: item.categoriaType,
slug: item.categoryId,
+ icon: item.categoriaIcon,
});
}
});
@@ -67,6 +69,8 @@ export function buildReadOnlyOptionSets(
(option) => ({
slug: option.value,
label: option.label,
+ type: option.group,
+ icon: option.icon,
}),
);
diff --git a/src/features/transactions/components/table/transaction-actions-menu.tsx b/src/features/transactions/components/table/transaction-actions-menu.tsx
new file mode 100644
index 0000000..985363f
--- /dev/null
+++ b/src/features/transactions/components/table/transaction-actions-menu.tsx
@@ -0,0 +1,157 @@
+"use client";
+
+import {
+ RiCheckLine,
+ RiDeleteBin5Line,
+ RiFileCopyLine,
+ RiFileList2Line,
+ RiHistoryLine,
+ RiMoreFill,
+ RiPencilLine,
+ RiRefundLine,
+ RiTimeLine,
+} from "@remixicon/react";
+import { Button } from "@/shared/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/shared/components/ui/dropdown-menu";
+import { REFUND_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
+import type { TransactionItem } from "../types";
+
+type TransactionActionsMenuProps = {
+ item: TransactionItem;
+ currentUserId: string;
+ onEdit?: (item: TransactionItem) => void;
+ onCopy?: (item: TransactionItem) => void;
+ onImport?: (item: TransactionItem) => void;
+ onConfirmDelete?: (item: TransactionItem) => void;
+ onViewDetails?: (item: TransactionItem) => void;
+ onRefund?: (item: TransactionItem) => void;
+ onAnticipate?: (item: TransactionItem) => void;
+ onViewAnticipationHistory?: (item: TransactionItem) => void;
+};
+
+export function TransactionActionsMenu({
+ item,
+ currentUserId,
+ onEdit,
+ onCopy,
+ onImport,
+ onConfirmDelete,
+ onViewDetails,
+ onRefund,
+ onAnticipate,
+ onViewAnticipationHistory,
+}: TransactionActionsMenuProps) {
+ const isOwnData = item.userId === currentUserId;
+ const canRefund =
+ isOwnData &&
+ item.transactionType === "Despesa" &&
+ item.condition === "À vista" &&
+ !item.splitGroupId &&
+ !item.readonly &&
+ !item.note?.startsWith(REFUND_NOTE_PREFIX);
+ const showInstallmentActions =
+ isOwnData && item.condition === "Parcelado" && item.seriesId;
+
+ return (
+
+
+
+
+
+ onViewDetails?.(item)}
+ disabled={!onViewDetails}
+ >
+
+ Detalhes
+
+
+ {isOwnData ? (
+ onEdit?.(item)}
+ disabled={item.readonly || !onEdit}
+ >
+
+ Editar
+
+ ) : null}
+
+ {!item.readonly && isOwnData ? (
+ onCopy?.(item)} disabled={!onCopy}>
+
+ Copiar
+
+ ) : null}
+
+ {!item.readonly && !isOwnData ? (
+ onImport?.(item)}
+ disabled={!onImport}
+ >
+
+ Importar para Minha Conta
+
+ ) : null}
+
+ {canRefund ? (
+ onRefund?.(item)}
+ disabled={!onRefund}
+ >
+
+ Reembolso
+
+ ) : null}
+
+ {isOwnData ? (
+ onConfirmDelete?.(item)}
+ disabled={item.readonly || !onConfirmDelete}
+ >
+
+ Remover
+
+ ) : null}
+
+ {showInstallmentActions ? (
+ <>
+
+
+ {!item.isAnticipated && onAnticipate ? (
+ onAnticipate(item)}>
+
+ Antecipar Parcelas
+
+ ) : null}
+
+ {onViewAnticipationHistory ? (
+ onViewAnticipationHistory(item)}
+ >
+
+ Histórico de Antecipações
+
+ ) : null}
+
+ {item.isAnticipated ? (
+
+
+ Parcela Antecipada
+
+ ) : null}
+ >
+ ) : null}
+
+
+ );
+}
diff --git a/src/features/transactions/components/table/transaction-settlement-button.tsx b/src/features/transactions/components/table/transaction-settlement-button.tsx
new file mode 100644
index 0000000..0d87941
--- /dev/null
+++ b/src/features/transactions/components/table/transaction-settlement-button.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import {
+ RiBankCard2Line,
+ RiCheckboxBlankCircleLine,
+ RiCheckboxCircleFill,
+} from "@remixicon/react";
+import {
+ CREDIT_CARD_PAYMENT_METHOD,
+ SETTLEABLE_PAYMENT_METHODS,
+} from "@/features/transactions/lib/constants";
+import { Button } from "@/shared/components/ui/button";
+import { Spinner } from "@/shared/components/ui/spinner";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/shared/components/ui/tooltip";
+import { cn } from "@/shared/utils/ui";
+import type { TransactionItem } from "../types";
+
+type TransactionSettlementButtonProps = {
+ item: TransactionItem;
+ isLoading: boolean;
+ onToggle?: (item: TransactionItem) => void;
+};
+
+export function TransactionSettlementButton({
+ item,
+ isLoading,
+ onToggle,
+}: TransactionSettlementButtonProps) {
+ const isCreditCard = item.paymentMethod === CREDIT_CARD_PAYMENT_METHOD;
+ const canToggleSettlement = (
+ SETTLEABLE_PAYMENT_METHODS as readonly string[]
+ ).includes(item.paymentMethod);
+
+ if (!canToggleSettlement && !isCreditCard) {
+ return null;
+ }
+
+ if (isCreditCard) {
+ const invoicePaid = Boolean(item.isSettled);
+
+ return (
+
+
+
+
+
+
+
+ {invoicePaid
+ ? "Fatura paga"
+ : "Lançamentos de cartão de crédito são liquidados ao pagar a fatura"}
+
+
+ );
+ }
+
+ const settled = Boolean(item.isSettled);
+
+ return (
+
+
+
+
+
+ {settled ? "Desfazer pagamento" : "Marcar como pago"}
+
+
+ );
+}
diff --git a/src/features/transactions/components/table/transactions-columns.tsx b/src/features/transactions/components/table/transactions-columns.tsx
index ef3c79d..d51b06f 100644
--- a/src/features/transactions/components/table/transactions-columns.tsx
+++ b/src/features/transactions/components/table/transactions-columns.tsx
@@ -1,28 +1,13 @@
import {
RiAttachment2,
- RiBankCard2Line,
RiChat1Line,
- RiCheckboxBlankCircleLine,
- RiCheckboxCircleFill,
- RiCheckLine,
- RiDeleteBin5Line,
- RiFileCopyLine,
- RiFileList2Line,
RiGroupLine,
- RiHistoryLine,
- RiMoreFill,
- RiPencilLine,
- RiRefundLine,
RiTimeLine,
} from "@remixicon/react";
import type { ColumnDef } from "@tanstack/react-table";
import Image from "next/image";
import Link from "next/link";
import { DEFAULT_TRANSACTIONS_COLUMN_ORDER } from "@/features/transactions/lib/column-order";
-import {
- CREDIT_CARD_PAYMENT_METHOD,
- SETTLEABLE_PAYMENT_METHODS,
-} from "@/features/transactions/lib/constants";
import {
CategoryIconBadge,
EstablishmentLogo,
@@ -35,28 +20,20 @@ import {
AvatarImage,
} from "@/shared/components/ui/avatar";
import { Badge } from "@/shared/components/ui/badge";
-import { Button } from "@/shared/components/ui/button";
import { Checkbox } from "@/shared/components/ui/checkbox";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/shared/components/ui/dropdown-menu";
-import { Spinner } from "@/shared/components/ui/spinner";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
-import { REFUND_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
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 type { TransactionItem } from "../types";
+import { TransactionActionsMenu } from "./transaction-actions-menu";
+import { TransactionSettlementButton } from "./transaction-settlement-button";
type BuildColumnsArgs = {
currentUserId: string;
@@ -551,195 +528,23 @@ function buildColumns({
enableSorting: false,
cell: ({ row }) => (
- {(() => {
- const paymentMethod = row.original.paymentMethod;
- const isCreditCard = paymentMethod === CREDIT_CARD_PAYMENT_METHOD;
- const canToggleSettlement = (
- SETTLEABLE_PAYMENT_METHODS as readonly string[]
- ).includes(paymentMethod);
-
- if (!canToggleSettlement && !isCreditCard) return null;
-
- if (isCreditCard) {
- const invoicePaid = Boolean(row.original.isSettled);
- return (
-
-
-
-
-
-
-
- {invoicePaid
- ? "Fatura paga"
- : "Lançamentos de cartão de crédito são liquidados ao pagar a fatura"}
-
-
- );
- }
-
- 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.readonly &&
- row.original.userId === currentUserId && (
- handleCopy(row.original)}>
-
- Copiar
-
- )}
- {!row.original.readonly &&
- row.original.userId !== currentUserId && (
- handleImport(row.original)}>
-
- Importar para Minha Conta
-
- )}
- {(() => {
- const item = row.original;
- const canRefund =
- item.userId === currentUserId &&
- item.transactionType === "Despesa" &&
- item.condition === "À vista" &&
- !item.splitGroupId &&
- !item.readonly &&
- !item.note?.startsWith(REFUND_NOTE_PREFIX);
-
- if (!canRefund) return null;
-
- return (
- handleRefund(item)}>
-
- Reembolso
-
- );
- })()}
- {row.original.userId === currentUserId && (
- handleConfirmDelete(row.original)}
- disabled={row.original.readonly}
- >
-
- Remover
-
- )}
-
- {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
-
- )}
- >
- )}
-
-
+
+
),
});
diff --git a/src/features/transactions/components/table/transactions-filters.tsx b/src/features/transactions/components/table/transactions-filters.tsx
index 6eaf150..5756015 100644
--- a/src/features/transactions/components/table/transactions-filters.tsx
+++ b/src/features/transactions/components/table/transactions-filters.tsx
@@ -67,6 +67,7 @@ import {
SelectTrigger,
} from "@/shared/components/ui/select";
import { Separator } from "@/shared/components/ui/separator";
+import { Spinner } from "@/shared/components/ui/spinner";
import { Switch } from "@/shared/components/ui/switch";
import {
ToggleGroup,
@@ -193,6 +194,16 @@ type MultiOption = {
render?: ReactNode;
};
+const getCategoryFilterGroup = (type?: string | null) => {
+ if (type === "receita") {
+ return "Receitas";
+ }
+ if (type === "despesa") {
+ return "Despesas";
+ }
+ return "Outras";
+};
+
interface MultiSelectFilterProps {
placeholder: string;
options: MultiOption[];
@@ -290,7 +301,10 @@ function MultiSelectFilter({
>
{triggerLabel}
-
+
@@ -331,7 +345,10 @@ function MultiSelectFilter({
{option.render ?? option.label}
{isSelected ? (
-
+
) : null}
);
@@ -518,6 +535,7 @@ export function TransactionsFilters({
categoryOptions.map((option) => ({
value: option.slug,
label: option.label,
+ group: getCategoryFilterGroup(option.type),
render: (
),
@@ -585,6 +603,7 @@ export function TransactionsFilters({
return (
-
+
) : null}
@@ -630,12 +649,19 @@ export function TransactionsFilters({
@@ -649,7 +675,7 @@ export function TransactionsFilters({
aria-label="Limpar filtros"
className="text-xs text-muted-foreground hover:text-foreground h-9 px-2"
>
-
+
Limpar
)}
@@ -746,6 +772,7 @@ export function TransactionsFilters({
disabled={isPending}
searchable
searchPlaceholder="Buscar categoria..."
+ groupOrder={["Despesas", "Receitas", "Outras"]}
/>
@@ -775,7 +802,7 @@ export function TransactionsFilters({
{hasDateRangeFilter ? (