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

@@ -55,8 +55,8 @@ interface AnticipateInstallmentsDialogProps {
type AnticipationFormValues = {
anticipationPeriod: string;
discount: string;
pagadorId: string;
categoriaId: string;
payerId: string;
categoryId: string;
note: string;
};
@@ -90,8 +90,8 @@ export function AnticipateInstallmentsDialog({
useFormState<AnticipationFormValues>({
anticipationPeriod: defaultPeriod,
discount: "0",
pagadorId: "",
categoriaId: "",
payerId: "",
categoryId: "",
note: "",
});
@@ -119,8 +119,8 @@ export function AnticipateInstallmentsDialog({
replaceForm({
anticipationPeriod: defaultPeriod,
discount: "0",
pagadorId: first.pagadorId ?? "",
categoriaId: first.categoriaId ?? "",
payerId: first.payerId ?? "",
categoryId: first.categoryId ?? "",
note: "",
});
}
@@ -182,8 +182,8 @@ export function AnticipateInstallmentsDialog({
installmentIds: selectedIds,
anticipationPeriod: formState.anticipationPeriod,
discount: Number(formState.discount) || 0,
pagadorId: formState.pagadorId || undefined,
categoriaId: formState.categoriaId || undefined,
payerId: formState.payerId || undefined,
categoryId: formState.categoryId || undefined,
note: formState.note || undefined,
});
@@ -269,11 +269,11 @@ export function AnticipateInstallmentsDialog({
</Field>
<Field className="gap-1">
<FieldLabel htmlFor="anticipation-pagador">Pagador</FieldLabel>
<FieldLabel htmlFor="anticipation-pagador">Payer</FieldLabel>
<FieldContent>
<Select
value={formState.pagadorId}
onValueChange={(value) => updateField("pagadorId", value)}
value={formState.payerId}
onValueChange={(value) => updateField("payerId", value)}
disabled={isPending}
>
<SelectTrigger id="anticipation-pagador" className="w-full">
@@ -292,12 +292,12 @@ export function AnticipateInstallmentsDialog({
<Field className="gap-1">
<FieldLabel htmlFor="anticipation-categoria">
Categoria
Category
</FieldLabel>
<FieldContent>
<Select
value={formState.categoriaId}
onValueChange={(value) => updateField("categoriaId", value)}
value={formState.categoryId}
onValueChange={(value) => updateField("categoryId", value)}
disabled={isPending}
>
<SelectTrigger

View File

@@ -29,7 +29,7 @@ interface AnticipationHistoryDialogProps {
lancamentoName: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
onViewLancamento?: (lancamentoId: string) => void;
onViewLancamento?: (transactionId: string) => void;
}
export function AnticipationHistoryDialog({

View File

@@ -2,8 +2,8 @@
import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { createLancamentoAction } from "@/features/transactions/actions";
import { groupAndSortCategorias } from "@/features/transactions/categoria-helpers";
import { createTransactionAction } from "@/features/transactions/actions";
import { groupAndSortCategories } from "@/features/transactions/category-helpers";
import { Button } from "@/shared/components/ui/button";
import {
Dialog,
@@ -24,46 +24,46 @@ import {
SelectValue,
} from "@/shared/components/ui/select";
import {
CategoriaSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
CategorySelectContent,
AccountCardSelectContent,
PayerSelectContent,
} from "../select-items";
import type { LancamentoItem, SelectOption } from "../types";
import type { SelectOption, TransactionItem } from "../types";
interface BulkImportDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
items: LancamentoItem[];
pagadorOptions: SelectOption[];
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
defaultPagadorId?: string | null;
items: TransactionItem[];
payerOptions: SelectOption[];
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
defaultPayerId?: string | null;
}
export function BulkImportDialog({
open,
onOpenChange,
items,
pagadorOptions,
contaOptions,
cartaoOptions,
categoriaOptions,
defaultPagadorId,
payerOptions,
accountOptions,
cardOptions,
categoryOptions,
defaultPayerId,
}: BulkImportDialogProps) {
const [pagadorId, setPagadorId] = useState<string | undefined>(
defaultPagadorId ?? undefined,
const [payerId, setPagadorId] = useState<string | undefined>(
defaultPayerId ?? undefined,
);
const [categoriaId, setCategoriaId] = useState<string | undefined>(undefined);
const [contaId, setContaId] = useState<string | undefined>(undefined);
const [cartaoId, setCartaoId] = useState<string | undefined>(undefined);
const [categoryId, setCategoriaId] = useState<string | undefined>(undefined);
const [accountId, setContaId] = useState<string | undefined>(undefined);
const [cardId, setCartaoId] = useState<string | undefined>(undefined);
const [isPending, startTransition] = useTransition();
type CreateLancamentoInput = Parameters<typeof createLancamentoAction>[0];
type CreateTransactionInput = Parameters<typeof createTransactionAction>[0];
// Reset form when dialog opens/closes
const handleOpenChange = (newOpen: boolean) => {
if (!newOpen) {
setPagadorId(defaultPagadorId ?? undefined);
setPagadorId(defaultPayerId ?? undefined);
setCategoriaId(undefined);
setContaId(undefined);
setCartaoId(undefined);
@@ -71,30 +71,30 @@ export function BulkImportDialog({
onOpenChange(newOpen);
};
const categoriaGroups = useMemo(() => {
const categoryGroups = useMemo(() => {
// Get unique transaction types from items
const transactionTypes = new Set(items.map((item) => item.transactionType));
// Filter categories based on transaction types
const filtered = categoriaOptions.filter((option) => {
const filtered = categoryOptions.filter((option) => {
if (!option.group) return false;
return Array.from(transactionTypes).some(
(type) => option.group?.toLowerCase() === type.toLowerCase(),
);
});
return groupAndSortCategorias(filtered);
}, [categoriaOptions, items]);
return groupAndSortCategories(filtered);
}, [categoryOptions, items]);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!pagadorId) {
if (!payerId) {
toast.error("Selecione o pagador.");
return;
}
if (!categoriaId) {
if (!categoryId) {
toast.error("Selecione a categoria.");
return;
}
@@ -110,32 +110,32 @@ export function BulkImportDialog({
const isCredit = item.paymentMethod === "Cartão de crédito";
// Validate payment method fields
if (isCredit && !cartaoId) {
if (isCredit && !cardId) {
toast.error("Selecione um cartão de crédito.");
return;
}
if (!isCredit && !contaId) {
if (!isCredit && !accountId) {
toast.error("Selecione uma conta.");
return;
}
const payload: CreateLancamentoInput = {
const payload: CreateTransactionInput = {
purchaseDate: item.purchaseDate,
period: item.period,
name: item.name,
transactionType:
item.transactionType as CreateLancamentoInput["transactionType"],
item.transactionType as CreateTransactionInput["transactionType"],
amount: sanitizedAmount,
condition: item.condition as CreateLancamentoInput["condition"],
condition: item.condition as CreateTransactionInput["condition"],
paymentMethod:
item.paymentMethod as CreateLancamentoInput["paymentMethod"],
pagadorId: pagadorId ?? null,
secondaryPagadorId: undefined,
item.paymentMethod as CreateTransactionInput["paymentMethod"],
payerId: payerId ?? null,
secondaryPayerId: undefined,
isSplit: false,
contaId: isCredit ? null : (contaId ?? null),
cartaoId: isCredit ? (cartaoId ?? null) : null,
categoriaId: categoriaId ?? null,
accountId: isCredit ? null : (accountId ?? null),
cardId: isCredit ? (cardId ?? null) : null,
categoryId: categoryId ?? null,
note: item.note ?? null,
isSettled: isCredit ? null : Boolean(item.isSettled),
installmentCount:
@@ -152,7 +152,7 @@ export function BulkImportDialog({
: undefined,
};
const result = await createLancamentoAction(payload);
const result = await createTransactionAction(payload);
if (result.success) {
successCount++;
@@ -203,17 +203,17 @@ export function BulkImportDialog({
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="pagador">Pagador *</Label>
<Select value={pagadorId} onValueChange={setPagadorId}>
<Label htmlFor="pagador">Payer *</Label>
<Select value={payerId} onValueChange={setPagadorId}>
<SelectTrigger id="pagador" className="w-full">
<SelectValue placeholder="Selecione o pagador">
{pagadorId &&
{payerId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === pagadorId,
const selectedOption = payerOptions.find(
(opt) => opt.value === payerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -222,9 +222,9 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
{payerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -235,17 +235,17 @@ export function BulkImportDialog({
</div>
<div className="space-y-2">
<Label htmlFor="categoria">Categoria *</Label>
<Select value={categoriaId} onValueChange={setCategoriaId}>
<Label htmlFor="categoria">Category *</Label>
<Select value={categoryId} onValueChange={setCategoriaId}>
<SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione a categoria">
{categoriaId &&
{categoryId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === categoriaId,
const selectedOption = categoryOptions.find(
(opt) => opt.value === categoryId,
);
return selectedOption ? (
<CategoriaSelectContent
<CategorySelectContent
label={selectedOption.label}
icon={selectedOption.icon}
/>
@@ -254,12 +254,12 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{categoriaGroups.map((group) => (
{categoryGroups.map((group) => (
<SelectGroup key={group.label}>
<SelectLabel>{group.label}</SelectLabel>
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>
@@ -274,18 +274,18 @@ export function BulkImportDialog({
{hasNonCredit && (
<div className="space-y-2">
<Label htmlFor="conta">
Conta {hasCredit ? "(para não cartão)" : "*"}
FinancialAccount {hasCredit ? "(para não cartão)" : "*"}
</Label>
<Select value={contaId} onValueChange={setContaId}>
<Select value={accountId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione a conta">
{contaId &&
{accountId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId,
const selectedOption = accountOptions.find(
(opt) => opt.value === accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -295,9 +295,9 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.map((option) => (
{accountOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -314,16 +314,16 @@ export function BulkImportDialog({
<Label htmlFor="cartao">
Cartão {hasNonCredit ? "(para cartão de crédito)" : "*"}
</Label>
<Select value={cartaoId} onValueChange={setCartaoId}>
<Select value={cardId} onValueChange={setCartaoId}>
<SelectTrigger id="cartao" className="w-full">
<SelectValue placeholder="Selecione o cartão">
{cartaoId &&
{cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -333,9 +333,9 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{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

@@ -3,10 +3,10 @@
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { groupAndSortCategorias } from "@/features/transactions/categoria-helpers";
import { groupAndSortCategories } from "@/features/transactions/category-helpers";
import {
LANCAMENTO_PAYMENT_METHODS,
type LANCAMENTO_TRANSACTION_TYPES,
PAYMENT_METHODS,
type TRANSACTION_TYPES,
} from "@/features/transactions/constants";
import { Button } from "@/shared/components/ui/button";
import { CurrencyInput } from "@/shared/components/ui/currency-input";
@@ -44,9 +44,9 @@ import {
periodToDate,
} from "@/shared/utils/period";
import {
CategoriaSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
CategorySelectContent,
AccountCardSelectContent,
PayerSelectContent,
PaymentMethodSelectContent,
TransactionTypeSelectContent,
} from "../select-items";
@@ -54,11 +54,9 @@ import { EstabelecimentoInput } from "../shared/establishment-input";
import type { SelectOption } from "../types";
/** Payment methods sem Boleto para este modal */
const MASS_ADD_PAYMENT_METHODS = LANCAMENTO_PAYMENT_METHODS.filter(
(m) => m !== "Boleto",
);
type MassAddTransactionType = (typeof LANCAMENTO_TRANSACTION_TYPES)[number];
type MassAddPaymentMethod = (typeof LANCAMENTO_PAYMENT_METHODS)[number];
const MASS_ADD_PAYMENT_METHODS = PAYMENT_METHODS.filter((m) => m !== "Boleto");
type MassAddTransactionType = (typeof TRANSACTION_TYPES)[number];
type MassAddPaymentMethod = (typeof PAYMENT_METHODS)[number];
function InlinePeriodPicker({
period,
@@ -71,7 +69,7 @@ function InlinePeriodPicker({
return (
<div className="-mt-1">
<span className="text-xs text-muted-foreground">Fatura de </span>
<span className="text-xs text-muted-foreground">Invoice de </span>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
@@ -99,18 +97,18 @@ interface MassAddDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: MassAddFormData) => Promise<void>;
pagadorOptions: SelectOption[];
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
payerOptions: SelectOption[];
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
estabelecimentos: string[];
selectedPeriod: string;
defaultPagadorId?: string | null;
defaultCartaoId?: string | null;
defaultPayerId?: string | null;
defaultCardId?: string | null;
}
export type MassAddFormData = Parameters<
typeof import("@/features/transactions/actions").createMassLancamentosAction
typeof import("@/features/transactions/actions").createMassTransactionsAction
>[0];
interface TransactionRow {
@@ -118,22 +116,22 @@ interface TransactionRow {
purchaseDate: string;
name: string;
amount: string;
categoriaId: string | undefined;
pagadorId: string | undefined;
categoryId: string | undefined;
payerId: string | undefined;
}
export function MassAddDialog({
open,
onOpenChange,
onSubmit,
pagadorOptions,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
accountOptions,
cardOptions,
categoryOptions,
estabelecimentos,
selectedPeriod,
defaultPagadorId,
defaultCartaoId,
defaultPayerId,
defaultCardId,
}: MassAddDialogProps) {
const [loading, setLoading] = useState(false);
@@ -141,16 +139,16 @@ export function MassAddDialog({
const [transactionType, setTransactionType] =
useState<MassAddTransactionType>("Despesa");
const [paymentMethod, setPaymentMethod] = useState<MassAddPaymentMethod>(
LANCAMENTO_PAYMENT_METHODS[0],
PAYMENT_METHODS[0],
);
const [period, setPeriod] = useState<string>(selectedPeriod);
const [contaId, setContaId] = useState<string | undefined>(undefined);
const [cartaoId, setCartaoId] = useState<string | undefined>(
defaultCartaoId ?? undefined,
const [accountId, setContaId] = useState<string | undefined>(undefined);
const [cardId, setCartaoId] = useState<string | undefined>(
defaultCardId ?? undefined,
);
// Quando defaultCartaoId está definido, exibe apenas o cartão específico
const isLockedToCartao = !!defaultCartaoId;
// Quando defaultCardId está definido, exibe apenas o cartão específico
const isLockedToCartao = !!defaultCardId;
const isCartaoSelected = paymentMethod === "Cartão de crédito";
@@ -161,18 +159,18 @@ export function MassAddDialog({
purchaseDate: getTodayDateString(),
name: "",
amount: "",
categoriaId: undefined,
pagadorId: defaultPagadorId ?? undefined,
categoryId: undefined,
payerId: defaultPayerId ?? undefined,
},
]);
// Categorias agrupadas e filtradas por tipo de transação
const groupedCategorias = useMemo(() => {
const filtered = categoriaOptions.filter(
const filtered = categoryOptions.filter(
(option) => option.group?.toLowerCase() === transactionType.toLowerCase(),
);
return groupAndSortCategorias(filtered);
}, [categoriaOptions, transactionType]);
return groupAndSortCategories(filtered);
}, [categoryOptions, transactionType]);
const addTransaction = () => {
setTransactions([
@@ -182,8 +180,8 @@ export function MassAddDialog({
purchaseDate: getTodayDateString(),
name: "",
amount: "",
categoriaId: undefined,
pagadorId: defaultPagadorId ?? undefined,
categoryId: undefined,
payerId: defaultPayerId ?? undefined,
},
]);
};
@@ -208,11 +206,11 @@ export function MassAddDialog({
const handleSubmit = async () => {
// Validate conta/cartao selection
if (isCartaoSelected && !cartaoId) {
if (isCartaoSelected && !cardId) {
toast.error("Selecione um cartão para continuar");
return;
}
if (!isCartaoSelected && !contaId) {
if (!isCartaoSelected && !accountId) {
toast.error("Selecione uma conta para continuar");
return;
}
@@ -236,15 +234,15 @@ export function MassAddDialog({
paymentMethod,
condition: "À vista",
period,
contaId,
cartaoId,
accountId,
cardId,
},
transactions: transactions.map((t) => ({
purchaseDate: t.purchaseDate,
name: t.name.trim(),
amount: Number(t.amount.trim()),
categoriaId: t.categoriaId,
pagadorId: t.pagadorId,
categoryId: t.categoryId,
payerId: t.payerId,
})),
};
@@ -254,18 +252,18 @@ export function MassAddDialog({
onOpenChange(false);
// Reset form
setTransactionType("Despesa");
setPaymentMethod(LANCAMENTO_PAYMENT_METHODS[0]);
setPaymentMethod(PAYMENT_METHODS[0]);
setPeriod(selectedPeriod);
setContaId(undefined);
setCartaoId(defaultCartaoId ?? undefined);
setCartaoId(defaultCardId ?? undefined);
setTransactions([
{
id: crypto.randomUUID(),
purchaseDate: getTodayDateString(),
name: "",
amount: "",
categoriaId: undefined,
pagadorId: defaultPagadorId ?? undefined,
categoryId: undefined,
payerId: defaultPayerId ?? undefined,
},
]);
} catch (_error) {
@@ -356,19 +354,19 @@ export function MassAddDialog({
<div className="space-y-2">
<Label htmlFor="cartao">Cartão</Label>
<Select
value={cartaoId}
value={cardId}
onValueChange={setCartaoId}
disabled={isLockedToCartao}
>
<SelectTrigger id="cartao" className="w-full">
<SelectValue placeholder="Selecione">
{cartaoId &&
{cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -378,22 +376,22 @@ export function MassAddDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
{cardOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions
cardOptions
.filter(
(option) =>
!isLockedToCartao ||
option.value === defaultCartaoId,
option.value === defaultCardId,
)
.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
@@ -403,7 +401,7 @@ export function MassAddDialog({
)}
</SelectContent>
</Select>
{cartaoId ? (
{cardId ? (
<InlinePeriodPicker
period={period}
onPeriodChange={setPeriod}
@@ -412,20 +410,20 @@ export function MassAddDialog({
</div>
) : null}
{/* Conta (for non-credit-card methods) */}
{/* FinancialAccount (for non-credit-card methods) */}
{!isCartaoSelected ? (
<div className="space-y-2">
<Label htmlFor="conta">Conta</Label>
<Select value={contaId} onValueChange={setContaId}>
<Label htmlFor="conta">FinancialAccount</Label>
<Select value={accountId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione">
{contaId &&
{accountId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId,
const selectedOption = accountOptions.find(
(opt) => opt.value === accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -435,16 +433,16 @@ export function MassAddDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.length === 0 ? (
{accountOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhuma conta cadastrada
</p>
</div>
) : (
contaOptions.map((option) => (
accountOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -536,26 +534,26 @@ export function MassAddDialog({
htmlFor={`pagador-${transaction.id}`}
className="sr-only"
>
Pagador {index + 1}
Payer {index + 1}
</Label>
<Select
value={transaction.pagadorId}
value={transaction.payerId}
onValueChange={(value) =>
updateTransaction(transaction.id, "pagadorId", value)
updateTransaction(transaction.id, "payerId", value)
}
>
<SelectTrigger
id={`pagador-${transaction.id}`}
className="w-32 truncate"
>
<SelectValue placeholder="Pagador">
{transaction.pagadorId &&
<SelectValue placeholder="Payer">
{transaction.payerId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === transaction.pagadorId,
const selectedOption = payerOptions.find(
(opt) => opt.value === transaction.payerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -564,9 +562,9 @@ export function MassAddDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
{payerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -581,23 +579,19 @@ export function MassAddDialog({
htmlFor={`categoria-${transaction.id}`}
className="sr-only"
>
Categoria {index + 1}
Category {index + 1}
</Label>
<Select
value={transaction.categoriaId}
value={transaction.categoryId}
onValueChange={(value) =>
updateTransaction(
transaction.id,
"categoriaId",
value,
)
updateTransaction(transaction.id, "categoryId", value)
}
>
<SelectTrigger
id={`categoria-${transaction.id}`}
className="w-32 truncate"
>
<SelectValue placeholder="Categoria" />
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
{groupedCategorias.map((group) => (
@@ -608,7 +602,7 @@ export function MassAddDialog({
key={option.value}
value={option.value}
>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>

View File

@@ -25,29 +25,29 @@ import { Separator } from "@/shared/components/ui/separator";
import { parseLocalDateString } from "@/shared/utils/date";
import { getPaymentMethodIcon } from "@/shared/utils/icons";
import { InstallmentTimeline } from "../shared/installment-timeline";
import type { LancamentoItem } from "../types";
import type { TransactionItem } from "../types";
interface LancamentoDetailsDialogProps {
interface TransactionDetailsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
lancamento: LancamentoItem | null;
transaction: TransactionItem | null;
}
export function LancamentoDetailsDialog({
export function TransactionDetailsDialog({
open,
onOpenChange,
lancamento,
}: LancamentoDetailsDialogProps) {
if (!lancamento) return null;
transaction,
}: TransactionDetailsDialogProps) {
if (!transaction) return null;
const isInstallment =
lancamento.condition?.toLowerCase() === "parcelado" &&
lancamento.currentInstallment &&
lancamento.installmentCount;
transaction.condition?.toLowerCase() === "parcelado" &&
transaction.currentInstallment &&
transaction.installmentCount;
const valorParcela = Math.abs(lancamento.amount);
const totalParcelas = lancamento.installmentCount ?? 1;
const parcelaAtual = lancamento.currentInstallment ?? 1;
const valorParcela = Math.abs(transaction.amount);
const totalParcelas = transaction.installmentCount ?? 1;
const parcelaAtual = transaction.currentInstallment ?? 1;
const valorTotal = isInstallment
? valorParcela * totalParcelas
: valorParcela;
@@ -62,10 +62,10 @@ export function LancamentoDetailsDialog({
<CardHeader className="flex flex-row items-start border-b sm:border-b-0">
<div>
<DialogTitle className="group flex items-center gap-2 text-lg">
#{lancamento.id}
#{transaction.id}
</DialogTitle>
<CardDescription>
{formatDate(lancamento.purchaseDate)}
{formatDate(transaction.purchaseDate)}
</CardDescription>
</div>
</CardHeader>
@@ -73,11 +73,11 @@ export function LancamentoDetailsDialog({
<CardContent className="text-sm">
<div className="grid gap-3">
<ul className="grid gap-3">
<DetailRow label="Descrição" value={lancamento.name} />
<DetailRow label="Descrição" value={transaction.name} />
<DetailRow
label="Período"
value={formatPeriod(lancamento.period)}
value={formatPeriod(transaction.period)}
/>
<li className="flex items-center justify-between">
@@ -85,21 +85,21 @@ export function LancamentoDetailsDialog({
Forma de Pagamento
</span>
<span className="flex items-center gap-1.5">
{getPaymentMethodIcon(lancamento.paymentMethod)}
{getPaymentMethodIcon(transaction.paymentMethod)}
<span className="capitalize">
{lancamento.paymentMethod}
{transaction.paymentMethod}
</span>
</span>
</li>
<DetailRow
label={lancamento.cartaoName ? "Cartão" : "Conta"}
value={lancamento.cartaoName ?? lancamento.contaName ?? "—"}
label={transaction.cartaoName ? "Cartão" : "FinancialAccount"}
value={transaction.cartaoName ?? transaction.contaName ?? "—"}
/>
<DetailRow
label="Categoria"
value={lancamento.categoriaName ?? "—"}
label="Category"
value={transaction.categoriaName ?? "—"}
/>
<li className="flex items-center justify-between">
@@ -109,37 +109,37 @@ export function LancamentoDetailsDialog({
<span className="capitalize">
<Badge
variant={getTransactionBadgeVariant(
lancamento.categoriaName === "Saldo inicial"
transaction.categoriaName === "Saldo inicial"
? "Saldo inicial"
: lancamento.transactionType,
: transaction.transactionType,
)}
>
{lancamento.categoriaName === "Saldo inicial"
{transaction.categoriaName === "Saldo inicial"
? "Saldo Inicial"
: lancamento.transactionType}
: transaction.transactionType}
</Badge>
</span>
</li>
<DetailRow
label="Condição"
value={formatCondition(lancamento.condition)}
value={formatCondition(transaction.condition)}
/>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Responsável</span>
<span className="flex items-center gap-2 capitalize">
<span>{lancamento.pagadorName}</span>
<span>{transaction.pagadorName}</span>
</span>
</li>
<DetailRow
label="Status"
value={lancamento.isSettled ? "Pago" : "Pendente"}
value={transaction.isSettled ? "Pago" : "Pendente"}
/>
{lancamento.note && (
<DetailRow label="Notas" value={lancamento.note} />
{transaction.note && (
<DetailRow label="Notas" value={transaction.note} />
)}
</ul>
@@ -148,11 +148,11 @@ export function LancamentoDetailsDialog({
<li className="mt-4">
<InstallmentTimeline
purchaseDate={parseLocalDateString(
lancamento.purchaseDate,
transaction.purchaseDate,
)}
currentInstallment={parcelaAtual}
totalInstallments={totalParcelas}
period={lancamento.period}
period={transaction.period}
/>
</li>
)}
@@ -169,10 +169,10 @@ export function LancamentoDetailsDialog({
/>
)}
{lancamento.recurrenceCount && (
{transaction.recurrenceCount && (
<DetailRow
label="Quantidade de Recorrências"
value={`${lancamento.recurrenceCount} meses`}
value={`${transaction.recurrenceCount} meses`}
/>
)}

View File

@@ -1,6 +1,6 @@
"use client";
import { LANCAMENTO_TRANSACTION_TYPES } from "@/features/transactions/constants";
import { TRANSACTION_TYPES } from "@/features/transactions/constants";
import { Label } from "@/shared/components/ui/label";
import {
Select,
@@ -13,7 +13,7 @@ import {
} from "@/shared/components/ui/select";
import { cn } from "@/shared/utils/ui";
import {
CategoriaSelectContent,
CategorySelectContent,
TransactionTypeSelectContent,
} from "../../select-items";
import type { CategorySectionProps } from "./transaction-dialog-types";
@@ -21,8 +21,8 @@ import type { CategorySectionProps } from "./transaction-dialog-types";
export function CategorySection({
formState,
onFieldChange,
categoriaOptions,
categoriaGroups,
categoryOptions,
categoryGroups,
isUpdateMode,
hideTransactionType = false,
}: CategorySectionProps) {
@@ -47,13 +47,13 @@ export function CategorySection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_TRANSACTION_TYPES.filter(
(type) => type !== "Transferência",
).map((type) => (
<SelectItem key={type} value={type}>
<TransactionTypeSelectContent label={type} />
</SelectItem>
))}
{TRANSACTION_TYPES.filter((type) => type !== "Transferência").map(
(type) => (
<SelectItem key={type} value={type}>
<TransactionTypeSelectContent label={type} />
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
@@ -65,20 +65,20 @@ export function CategorySection({
showTransactionTypeField ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="categoria">Categoria</Label>
<Label htmlFor="categoria">Category</Label>
<Select
value={formState.categoriaId}
onValueChange={(value) => onFieldChange("categoriaId", value)}
value={formState.categoryId}
onValueChange={(value) => onFieldChange("categoryId", value)}
>
<SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione">
{formState.categoriaId &&
{formState.categoryId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === formState.categoriaId,
const selectedOption = categoryOptions.find(
(opt) => opt.value === formState.categoryId,
);
return selectedOption ? (
<CategoriaSelectContent
<CategorySelectContent
label={selectedOption.label}
icon={selectedOption.icon}
/>
@@ -87,12 +87,12 @@ export function CategorySection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{categoriaGroups.map((group) => (
{categoryGroups.map((group) => (
<SelectGroup key={group.label}>
<SelectLabel>{group.label}</SelectLabel>
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>

View File

@@ -1,6 +1,6 @@
"use client";
import { LANCAMENTO_CONDITIONS } from "@/features/transactions/constants";
import { TRANSACTION_CONDITIONS } from "@/features/transactions/constants";
import { Label } from "@/shared/components/ui/label";
import {
Select,
@@ -64,7 +64,7 @@ export function ConditionSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_CONDITIONS.map((condition) => (
{TRANSACTION_CONDITIONS.map((condition) => (
<SelectItem key={condition} value={condition}>
<ConditionSelectContent label={condition} />
</SelectItem>

View File

@@ -9,16 +9,16 @@ import {
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select";
import { PagadorSelectContent } from "../../select-items";
import type { PagadorSectionProps } from "./transaction-dialog-types";
import { PayerSelectContent } from "../../select-items";
import type { PayerSectionProps } from "./transaction-dialog-types";
export function PagadorSection({
export function PayerSection({
formState,
onFieldChange,
pagadorOptions,
secondaryPagadorOptions,
payerOptions,
secondaryPayerOptions,
totalAmount,
}: PagadorSectionProps) {
}: PayerSectionProps) {
const handlePrimaryAmountChange = (value: string) => {
onFieldChange("primarySplitAmount", value);
const numericValue = Number.parseFloat(value) || 0;
@@ -36,24 +36,24 @@ export function PagadorSection({
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-full space-y-1">
<Label htmlFor="pagador">Pagador</Label>
<Label htmlFor="payer">Payer</Label>
<div className="flex gap-2">
<Select
value={formState.pagadorId}
onValueChange={(value) => onFieldChange("pagadorId", value)}
value={formState.payerId}
onValueChange={(value) => onFieldChange("payerId", value)}
>
<SelectTrigger
id="pagador"
id="payer"
className={formState.isSplit ? "w-[55%]" : "w-full"}
>
<SelectValue placeholder="Selecione">
{formState.pagadorId &&
{formState.payerId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === formState.pagadorId,
const selectedOption = payerOptions.find(
(opt) => opt.value === formState.payerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -62,9 +62,9 @@ export function PagadorSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
{payerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -85,27 +85,27 @@ export function PagadorSection({
{formState.isSplit ? (
<div className="w-full space-y-1 mb-1">
<Label htmlFor="secondaryPagador">Dividir com</Label>
<Label htmlFor="secondaryPayer">Dividir com</Label>
<div className="flex gap-2">
<Select
value={formState.secondaryPagadorId}
value={formState.secondaryPayerId}
onValueChange={(value) =>
onFieldChange("secondaryPagadorId", value)
onFieldChange("secondaryPayerId", value)
}
>
<SelectTrigger
id="secondaryPagador"
disabled={secondaryPagadorOptions.length === 0}
id="secondaryPayer"
disabled={secondaryPayerOptions.length === 0}
className="w-[55%]"
>
<SelectValue placeholder="Selecione">
{formState.secondaryPagadorId &&
{formState.secondaryPayerId &&
(() => {
const selectedOption = secondaryPagadorOptions.find(
(opt) => opt.value === formState.secondaryPagadorId,
const selectedOption = secondaryPayerOptions.find(
(opt) => opt.value === formState.secondaryPayerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -114,9 +114,9 @@ export function PagadorSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{secondaryPagadorOptions.map((option) => (
{secondaryPayerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { LANCAMENTO_PAYMENT_METHODS } from "@/features/transactions/constants";
import { PAYMENT_METHODS } from "@/features/transactions/constants";
import { Label } from "@/shared/components/ui/label";
import { MonthPicker } from "@/shared/components/ui/month-picker";
import {
@@ -23,7 +23,7 @@ import {
} from "@/shared/utils/period";
import { cn } from "@/shared/utils/ui";
import {
ContaCartaoSelectContent,
AccountCardSelectContent,
PaymentMethodSelectContent,
} from "../../select-items";
import type { PaymentMethodSectionProps } from "./transaction-dialog-types";
@@ -39,7 +39,7 @@ function InlinePeriodPicker({
return (
<div className="ml-1">
<span className="text-xs text-muted-foreground">Fatura de </span>
<span className="text-xs text-muted-foreground">Invoice de </span>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
@@ -66,11 +66,11 @@ function InlinePeriodPicker({
export function PaymentMethodSection({
formState,
onFieldChange,
contaOptions,
cartaoOptions,
accountOptions,
cardOptions,
isUpdateMode,
disablePaymentMethod,
disableCartaoSelect,
disableCardSelect,
}: PaymentMethodSectionProps) {
const isCartaoSelected = formState.paymentMethod === "Cartão de crédito";
const showContaSelect = [
@@ -85,10 +85,10 @@ export function PaymentMethodSection({
// Filtrar contas apenas do tipo "Pré-Pago | VR/VA" quando forma de pagamento for "Pré-Pago | VR/VA"
const filteredContaOptions =
formState.paymentMethod === "Pré-Pago | VR/VA"
? contaOptions.filter(
? accountOptions.filter(
(option) => option.accountType === "Pré-Pago | VR/VA",
)
: contaOptions;
: accountOptions;
return (
<>
@@ -120,7 +120,7 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_PAYMENT_METHODS.map((method) => (
{PAYMENT_METHODS.map((method) => (
<SelectItem key={method} value={method}>
<PaymentMethodSelectContent label={method} />
</SelectItem>
@@ -133,23 +133,23 @@ export function PaymentMethodSection({
<div className="space-y-1 w-full md:w-1/2">
<Label htmlFor="cartao">Cartão</Label>
<Select
value={formState.cartaoId}
onValueChange={(value) => onFieldChange("cartaoId", value)}
disabled={disableCartaoSelect}
value={formState.cardId}
onValueChange={(value) => onFieldChange("cardId", value)}
disabled={disableCardSelect}
>
<SelectTrigger
id="cartao"
className="w-full"
disabled={disableCartaoSelect}
disabled={disableCardSelect}
>
<SelectValue placeholder="Selecione">
{formState.cartaoId &&
{formState.cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === formState.cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === formState.cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -159,16 +159,16 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
{cardOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions.map((option) => (
cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
@@ -178,7 +178,7 @@ export function PaymentMethodSection({
)}
</SelectContent>
</Select>
{formState.cartaoId ? (
{formState.cardId ? (
<InlinePeriodPicker
period={formState.period}
onPeriodChange={(value) => onFieldChange("period", value)}
@@ -194,20 +194,20 @@ export function PaymentMethodSection({
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="conta">Conta</Label>
<Label htmlFor="conta">FinancialAccount</Label>
<Select
value={formState.contaId}
onValueChange={(value) => onFieldChange("contaId", value)}
value={formState.accountId}
onValueChange={(value) => onFieldChange("accountId", value)}
>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione">
{formState.contaId &&
{formState.accountId &&
(() => {
const selectedOption = filteredContaOptions.find(
(opt) => opt.value === formState.contaId,
(opt) => opt.value === formState.accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -226,7 +226,7 @@ export function PaymentMethodSection({
) : (
filteredContaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -252,18 +252,18 @@ export function PaymentMethodSection({
>
<Label htmlFor="cartaoUpdate">Cartão</Label>
<Select
value={formState.cartaoId}
onValueChange={(value) => onFieldChange("cartaoId", value)}
value={formState.cardId}
onValueChange={(value) => onFieldChange("cardId", value)}
>
<SelectTrigger id="cartaoUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.cartaoId &&
{formState.cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === formState.cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === formState.cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -273,16 +273,16 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
{cardOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions.map((option) => (
cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
@@ -292,7 +292,7 @@ export function PaymentMethodSection({
)}
</SelectContent>
</Select>
{formState.cartaoId ? (
{formState.cardId ? (
<InlinePeriodPicker
period={formState.period}
onPeriodChange={(value) => onFieldChange("period", value)}
@@ -308,20 +308,20 @@ export function PaymentMethodSection({
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="contaUpdate">Conta</Label>
<Label htmlFor="contaUpdate">FinancialAccount</Label>
<Select
value={formState.contaId}
onValueChange={(value) => onFieldChange("contaId", value)}
value={formState.accountId}
onValueChange={(value) => onFieldChange("accountId", value)}
>
<SelectTrigger id="contaUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.contaId &&
{formState.accountId &&
(() => {
const selectedOption = filteredContaOptions.find(
(opt) => opt.value === formState.contaId,
(opt) => opt.value === formState.accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -340,7 +340,7 @@ export function PaymentMethodSection({
) : (
filteredContaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}

View File

@@ -1,28 +1,28 @@
import type { LancamentoFormState } from "@/features/transactions/form-helpers";
import type { LancamentoItem, SelectOption } from "../../types";
import type { TransactionFormState } from "@/features/transactions/form-helpers";
import type { SelectOption, TransactionItem } from "../../types";
export type FormState = LancamentoFormState;
export type FormState = TransactionFormState;
export interface LancamentoDialogProps {
export interface TransactionDialogProps {
mode: "create" | "update";
trigger?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
defaultPagadorId?: string | null;
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
payerOptions: SelectOption[];
splitPayerOptions: SelectOption[];
defaultPayerId?: string | null;
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
estabelecimentos: string[];
lancamento?: LancamentoItem;
transaction?: TransactionItem;
defaultPeriod?: string;
defaultCartaoId?: string | null;
defaultCardId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
defaultName?: string | null;
defaultAmount?: string | null;
lockCartaoSelection?: boolean;
lockCardSelection?: boolean;
lockPaymentMethod?: boolean;
isImporting?: boolean;
defaultTransactionType?: "Despesa" | "Receita";
@@ -33,11 +33,11 @@ export interface LancamentoDialogProps {
onBulkEditRequest?: (data: {
id: string;
name: string;
categoriaId: string | undefined;
categoryId: string | undefined;
note: string;
pagadorId: string | undefined;
contaId: string | undefined;
cartaoId: string | undefined;
payerId: string | undefined;
accountId: string | undefined;
cardId: string | undefined;
amount: number;
dueDate: string | null;
boletoPaymentDate: string | null;
@@ -57,8 +57,8 @@ export interface BasicFieldsSectionProps extends BaseFieldSectionProps {
}
export interface CategorySectionProps extends BaseFieldSectionProps {
categoriaOptions: SelectOption[];
categoriaGroups: Array<{
categoryOptions: SelectOption[];
categoryGroups: Array<{
label: string;
options: SelectOption[];
}>;
@@ -70,18 +70,18 @@ export interface SplitAndSettlementSectionProps extends BaseFieldSectionProps {
showSettledToggle: boolean;
}
export interface PagadorSectionProps extends BaseFieldSectionProps {
pagadorOptions: SelectOption[];
secondaryPagadorOptions: SelectOption[];
export interface PayerSectionProps extends BaseFieldSectionProps {
payerOptions: SelectOption[];
secondaryPayerOptions: SelectOption[];
totalAmount: number;
}
export interface PaymentMethodSectionProps extends BaseFieldSectionProps {
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
accountOptions: SelectOption[];
cardOptions: SelectOption[];
isUpdateMode: boolean;
disablePaymentMethod: boolean;
disableCartaoSelect: boolean;
disableCardSelect: boolean;
}
export interface BoletoFieldsSectionProps extends BaseFieldSectionProps {

View File

@@ -3,16 +3,16 @@ import { RiAddLine } from "@remixicon/react";
import { useEffect, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import {
createLancamentoAction,
updateLancamentoAction,
createTransactionAction,
updateTransactionAction,
} from "@/features/transactions/actions";
import {
filterSecondaryPagadorOptions,
groupAndSortCategorias,
} from "@/features/transactions/categoria-helpers";
filterSecondaryPayerOptions,
groupAndSortCategories,
} from "@/features/transactions/category-helpers";
import {
applyFieldDependencies,
buildLancamentoInitialState,
buildTransactionInitialState,
deriveCreditCardPeriod,
} from "@/features/transactions/form-helpers";
import { Button } from "@/shared/components/ui/button";
@@ -36,41 +36,41 @@ import { BoletoFieldsSection } from "./boleto-fields-section";
import { CategorySection } from "./category-section";
import { ConditionSection } from "./condition-section";
import { NoteSection } from "./note-section";
import { PagadorSection } from "./pagador-section";
import { PayerSection } from "./payer-section";
import { PaymentMethodSection } from "./payment-method-section";
import { SplitAndSettlementSection } from "./split-settlement-section";
import type {
FormState,
LancamentoDialogProps,
TransactionDialogProps,
} from "./transaction-dialog-types";
export function LancamentoDialog({
export function TransactionDialog({
mode,
trigger,
open,
onOpenChange,
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
estabelecimentos,
lancamento,
transaction,
defaultPeriod,
defaultCartaoId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
lockCartaoSelection,
lockCardSelection,
lockPaymentMethod,
isImporting = false,
defaultTransactionType,
forceShowTransactionType = false,
onSuccess,
onBulkEditRequest,
}: LancamentoDialogProps) {
}: TransactionDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
open,
false,
@@ -78,8 +78,8 @@ export function LancamentoDialog({
);
const [formState, setFormState] = useState<FormState>(() =>
buildLancamentoInitialState(lancamento, defaultPagadorId, defaultPeriod, {
defaultCartaoId,
buildTransactionInitialState(transaction, defaultPayerId, defaultPeriod, {
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
@@ -93,12 +93,12 @@ export function LancamentoDialog({
useEffect(() => {
if (dialogOpen) {
const initial = buildLancamentoInitialState(
lancamento,
defaultPagadorId,
const initial = buildTransactionInitialState(
transaction,
defaultPayerId,
defaultPeriod,
{
defaultCartaoId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
@@ -108,15 +108,13 @@ export function LancamentoDialog({
},
);
// Derive credit card period on open when cartaoId is pre-filled
// Derive credit card period on open when cardId is pre-filled
if (
initial.paymentMethod === "Cartão de crédito" &&
initial.cartaoId &&
initial.cardId &&
initial.purchaseDate
) {
const card = cartaoOptions.find(
(opt) => opt.value === initial.cartaoId,
);
const card = cardOptions.find((opt) => opt.value === initial.cardId);
if (card?.closingDay) {
initial.period = deriveCreditCardPeriod(
initial.purchaseDate,
@@ -131,45 +129,45 @@ export function LancamentoDialog({
}
}, [
dialogOpen,
lancamento,
defaultPagadorId,
transaction,
defaultPayerId,
defaultPeriod,
defaultCartaoId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
defaultTransactionType,
isImporting,
cartaoOptions,
cardOptions,
]);
const primaryPagador = formState.pagadorId;
const primaryPayerId = formState.payerId;
const secondaryPagadorOptions = useMemo(
() => filterSecondaryPagadorOptions(splitPagadorOptions, primaryPagador),
[splitPagadorOptions, primaryPagador],
const secondaryPayerOptions = useMemo(
() => filterSecondaryPayerOptions(splitPayerOptions, primaryPayerId),
[splitPayerOptions, primaryPayerId],
);
const categoriaGroups = useMemo(() => {
const filtered = categoriaOptions.filter(
const categoryGroups = useMemo(() => {
const filtered = categoryOptions.filter(
(option) =>
option.group?.toLowerCase() === formState.transactionType.toLowerCase(),
);
return groupAndSortCategorias(filtered);
}, [categoriaOptions, formState.transactionType]);
return groupAndSortCategories(filtered);
}, [categoryOptions, formState.transactionType]);
type CreateLancamentoInput = Parameters<typeof createLancamentoAction>[0];
type UpdateLancamentoInput = Parameters<typeof updateLancamentoAction>[0];
type CreateTransactionInput = Parameters<typeof createTransactionAction>[0];
type UpdateTransactionInput = Parameters<typeof updateTransactionAction>[0];
const totalAmount = useMemo(() => {
const parsed = Number.parseFloat(formState.amount);
return Number.isNaN(parsed) ? 0 : Math.abs(parsed);
}, [formState.amount]);
function getCardInfo(cartaoId: string | undefined) {
if (!cartaoId) return null;
const card = cartaoOptions.find((opt) => opt.value === cartaoId);
function getCardInfo(cardId: string | undefined) {
if (!cardId) return null;
const card = cardOptions.find((opt) => opt.value === cardId);
if (!card) return null;
return {
closingDay: card.closingDay ?? null,
@@ -182,9 +180,9 @@ export function LancamentoDialog({
value: FormState[Key],
) {
setFormState((prev) => {
const effectiveCartaoId =
key === "cartaoId" ? (value as string) : prev.cartaoId;
const cardInfo = getCardInfo(effectiveCartaoId);
const effectiveCardId =
key === "cardId" ? (value as string) : prev.cardId;
const cardInfo = getCardInfo(effectiveCardId);
const dependencies = applyFieldDependencies(key, value, prev, cardInfo);
@@ -214,7 +212,7 @@ export function LancamentoDialog({
return;
}
if (formState.isSplit && !formState.pagadorId) {
if (formState.isSplit && !formState.payerId) {
const message =
"Selecione o pagador principal para dividir o lançamento.";
setErrorMessage(message);
@@ -222,7 +220,7 @@ export function LancamentoDialog({
return;
}
if (formState.isSplit && !formState.secondaryPagadorId) {
if (formState.isSplit && !formState.secondaryPayerId) {
const message =
"Selecione o pagador secundário para dividir o lançamento.";
setErrorMessage(message);
@@ -240,7 +238,7 @@ export function LancamentoDialog({
const sanitizedAmount = Math.abs(amountValue);
if (!formState.categoriaId) {
if (!formState.categoryId) {
const message = "Selecione uma categoria.";
setErrorMessage(message);
toast.error(message);
@@ -248,32 +246,32 @@ export function LancamentoDialog({
}
if (formState.paymentMethod === "Cartão de crédito") {
if (!formState.cartaoId) {
if (!formState.cardId) {
const message = "Selecione o cartão.";
setErrorMessage(message);
toast.error(message);
return;
}
} else if (!formState.contaId) {
} else if (!formState.accountId) {
const message = "Selecione a conta.";
setErrorMessage(message);
toast.error(message);
return;
}
const payload: CreateLancamentoInput = {
const payload: CreateTransactionInput = {
purchaseDate: formState.purchaseDate,
period: formState.period,
name: formState.name.trim(),
transactionType:
formState.transactionType as CreateLancamentoInput["transactionType"],
formState.transactionType as CreateTransactionInput["transactionType"],
amount: sanitizedAmount,
condition: formState.condition as CreateLancamentoInput["condition"],
condition: formState.condition as CreateTransactionInput["condition"],
paymentMethod:
formState.paymentMethod as CreateLancamentoInput["paymentMethod"],
pagadorId: formState.pagadorId ?? null,
secondaryPagadorId: formState.isSplit
? formState.secondaryPagadorId
formState.paymentMethod as CreateTransactionInput["paymentMethod"],
payerId: formState.payerId ?? null,
secondaryPayerId: formState.isSplit
? formState.secondaryPayerId
: undefined,
isSplit: formState.isSplit,
primarySplitAmount: formState.isSplit
@@ -282,9 +280,9 @@ export function LancamentoDialog({
secondarySplitAmount: formState.isSplit
? Number.parseFloat(formState.secondarySplitAmount) || undefined
: undefined,
contaId: formState.contaId ?? null,
cartaoId: formState.cartaoId ?? null,
categoriaId: formState.categoriaId ?? null,
accountId: formState.accountId ?? null,
cardId: formState.cardId ?? null,
categoryId: formState.categoryId ?? null,
note: formState.note.trim() || null,
isSettled:
formState.paymentMethod === "Cartão de crédito"
@@ -309,7 +307,7 @@ export function LancamentoDialog({
startTransition(async () => {
if (mode === "create") {
const result = await createLancamentoAction(payload);
const result = await createTransactionAction(payload);
if (result.success) {
toast.success(result.message);
@@ -324,18 +322,18 @@ export function LancamentoDialog({
}
// Update mode
const hasSeriesId = Boolean(lancamento?.seriesId);
const hasSeriesId = Boolean(transaction?.seriesId);
if (hasSeriesId && onBulkEditRequest) {
// Para lançamentos em série, abre o diálogo de bulk action
onBulkEditRequest({
id: lancamento?.id ?? "",
id: transaction?.id ?? "",
name: formState.name.trim(),
categoriaId: formState.categoriaId,
categoryId: formState.categoryId,
note: formState.note.trim() || "",
pagadorId: formState.pagadorId,
contaId: formState.contaId,
cartaoId: formState.cartaoId,
payerId: formState.payerId,
accountId: formState.accountId,
cardId: formState.cardId,
amount: sanitizedAmount,
dueDate:
formState.paymentMethod === "Boleto"
@@ -350,12 +348,12 @@ export function LancamentoDialog({
}
// Atualização normal para lançamentos únicos ou todos os campos
const updatePayload: UpdateLancamentoInput = {
id: lancamento?.id ?? "",
const updatePayload: UpdateTransactionInput = {
id: transaction?.id ?? "",
...payload,
};
const result = await updateLancamentoAction(updatePayload);
const result = await updateTransactionAction(updatePayload);
if (result.success) {
toast.success(result.message);
@@ -369,15 +367,15 @@ export function LancamentoDialog({
});
};
const isCopyMode = mode === "create" && Boolean(lancamento) && !isImporting;
const isImportMode = mode === "create" && Boolean(lancamento) && isImporting;
const isCopyMode = mode === "create" && Boolean(transaction) && !isImporting;
const isImportMode = mode === "create" && Boolean(transaction) && isImporting;
const isNewWithType =
mode === "create" && !lancamento && defaultTransactionType;
mode === "create" && !transaction && defaultTransactionType;
const title =
mode === "create"
? isImportMode
? "Importar para Minha Conta"
? "Importar para Minha FinancialAccount"
: isCopyMode
? "Copiar lançamento"
: isNewWithType
@@ -405,7 +403,7 @@ export function LancamentoDialog({
const showSettledToggle = formState.paymentMethod !== "Cartão de crédito";
const isUpdateMode = mode === "update";
const disablePaymentMethod = Boolean(lockPaymentMethod && mode === "create");
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
const disableCardSelect = Boolean(lockCardSelection && mode === "create");
return (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -430,8 +428,8 @@ export function LancamentoDialog({
<CategorySection
formState={formState}
onFieldChange={handleFieldChange}
categoriaOptions={categoriaOptions}
categoriaGroups={categoriaGroups}
categoryOptions={categoryOptions}
categoryGroups={categoryGroups}
isUpdateMode={isUpdateMode}
hideTransactionType={
Boolean(isNewWithType) && !forceShowTransactionType
@@ -446,22 +444,22 @@ export function LancamentoDialog({
/>
) : null}
<PagadorSection
<PayerSection
formState={formState}
onFieldChange={handleFieldChange}
pagadorOptions={pagadorOptions}
secondaryPagadorOptions={secondaryPagadorOptions}
payerOptions={payerOptions}
secondaryPayerOptions={secondaryPayerOptions}
totalAmount={totalAmount}
/>
<PaymentMethodSection
formState={formState}
onFieldChange={handleFieldChange}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
accountOptions={accountOptions}
cardOptions={cardOptions}
isUpdateMode={isUpdateMode}
disablePaymentMethod={disablePaymentMethod}
disableCartaoSelect={disableCartaoSelect}
disableCardSelect={disableCardSelect}
/>
{showDueDate ? (